aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv
diff options
context:
space:
mode:
authorNick Chalko <nchalko@google.com>2016-02-26 13:38:57 -0800
committerNick Chalko <nchalko@google.com>2016-02-26 13:39:22 -0800
commitba5845f23b8fbc985890f892961abc8b39886611 (patch)
treeda373b9fe1955a2c7008c2e65df5ec3f5b087454 /src/com/android/tv
parent1abddd9f6225298066094e20a6c29061b6af4590 (diff)
downloadTV-ba5845f23b8fbc985890f892961abc8b39886611.tar.gz
Sync to ub-tv-interns at cc7c29d2a24a1343498f6d95ca5a79e003e6aefe
Change-Id: I580da190231e47c65b69f425b30ec4685eb50ce4
Diffstat (limited to 'src/com/android/tv')
-rw-r--r--src/com/android/tv/ApplicationSingletons.java5
-rw-r--r--src/com/android/tv/ChannelTuner.java4
-rw-r--r--src/com/android/tv/Features.java57
-rw-r--r--src/com/android/tv/MainActivity.java361
-rw-r--r--src/com/android/tv/MainActivityWrapper.java129
-rw-r--r--src/com/android/tv/SetupPassthroughActivity.java4
-rw-r--r--src/com/android/tv/TimeShiftManager.java74
-rw-r--r--src/com/android/tv/TvApplication.java131
-rw-r--r--src/com/android/tv/TvOptionsManager.java15
-rw-r--r--src/com/android/tv/analytics/OptOutPreferenceHelper.java112
-rw-r--r--src/com/android/tv/analytics/SendChannelStatusRunnable.java116
-rw-r--r--src/com/android/tv/analytics/StubAnalytics.java2
-rw-r--r--src/com/android/tv/data/Channel.java33
-rw-r--r--src/com/android/tv/data/ChannelDataManager.java92
-rw-r--r--src/com/android/tv/data/GenreItems.java35
-rw-r--r--src/com/android/tv/data/Program.java44
-rw-r--r--src/com/android/tv/data/ProgramDataManager.java25
-rw-r--r--src/com/android/tv/data/TvInputNewComparator.java10
-rw-r--r--src/com/android/tv/data/WatchedHistoryManager.java5
-rw-r--r--src/com/android/tv/dialog/FullscreenDialogFragment.java25
-rw-r--r--src/com/android/tv/dvr/BaseDvrDataManager.java8
-rw-r--r--src/com/android/tv/dvr/DvrDataManager.java9
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerImpl.java95
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java38
-rw-r--r--src/com/android/tv/dvr/DvrManager.java72
-rw-r--r--src/com/android/tv/dvr/DvrPlayActivity.java47
-rw-r--r--src/com/android/tv/dvr/DvrRecordingService.java20
-rw-r--r--src/com/android/tv/dvr/DvrSessionManager.java56
-rw-r--r--src/com/android/tv/dvr/Recording.java76
-rw-r--r--src/com/android/tv/dvr/RecordingTask.java242
-rw-r--r--src/com/android/tv/dvr/Scheduler.java73
-rw-r--r--src/com/android/tv/dvr/WritableDvrDataManager.java3
-rw-r--r--src/com/android/tv/dvr/provider/AsyncDvrDbTask.java72
-rw-r--r--src/com/android/tv/dvr/provider/DvrContract.java10
-rw-r--r--src/com/android/tv/dvr/provider/DvrDatabaseHelper.java93
-rw-r--r--src/com/android/tv/dvr/ui/DvrActivity.java (renamed from src/com/android/tv/util/MemoryManageable.java)22
-rw-r--r--src/com/android/tv/dvr/ui/DvrBrowseFragment.java119
-rw-r--r--src/com/android/tv/dvr/ui/GridItemPresenter.java165
-rw-r--r--src/com/android/tv/guide/ProgramGuide.java81
-rw-r--r--src/com/android/tv/guide/ProgramItemView.java103
-rw-r--r--src/com/android/tv/guide/ProgramManager.java4
-rw-r--r--src/com/android/tv/guide/ProgramTableAdapter.java74
-rw-r--r--src/com/android/tv/menu/ActionCardView.java1
-rw-r--r--src/com/android/tv/menu/AppLinkCardView.java35
-rw-r--r--src/com/android/tv/menu/ChannelCardView.java31
-rw-r--r--src/com/android/tv/menu/ChannelsPosterPrefetcher.java26
-rw-r--r--src/com/android/tv/menu/ChannelsRowAdapter.java74
-rw-r--r--src/com/android/tv/menu/MenuAction.java19
-rw-r--r--src/com/android/tv/menu/MenuRowView.java1
-rw-r--r--src/com/android/tv/menu/PlayControlsRowView.java20
-rw-r--r--src/com/android/tv/menu/SimpleCardView.java (renamed from src/com/android/tv/menu/GuideCardView.java)8
-rw-r--r--src/com/android/tv/menu/TvOptionsRowAdapter.java36
-rw-r--r--src/com/android/tv/onboarding/AppOverviewFragment.java110
-rw-r--r--src/com/android/tv/onboarding/NewSourcesFragment.java82
-rw-r--r--src/com/android/tv/onboarding/OnboardingActivity.java260
-rw-r--r--src/com/android/tv/onboarding/PagingIndicator.java235
-rw-r--r--src/com/android/tv/onboarding/SetupSourcesFragment.java428
-rw-r--r--src/com/android/tv/onboarding/WelcomeFragment.java828
-rw-r--r--src/com/android/tv/onboarding/WelcomePageFragment.java58
-rw-r--r--src/com/android/tv/receiver/AudioCapabilitiesReceiver.java5
-rw-r--r--src/com/android/tv/receiver/BootCompletedReceiver.java7
-rw-r--r--src/com/android/tv/receiver/PackageIntentsReceiver.java2
-rw-r--r--src/com/android/tv/recommendation/NotificationService.java162
-rw-r--r--src/com/android/tv/search/DataManagerSearch.java4
-rw-r--r--src/com/android/tv/search/LocalSearchProvider.java9
-rw-r--r--src/com/android/tv/search/ProgramGuideSearchFragment.java24
-rw-r--r--src/com/android/tv/search/TvProviderSearch.java14
-rw-r--r--src/com/android/tv/ui/AppLayerTvView.java4
-rw-r--r--src/com/android/tv/ui/ChannelBannerView.java55
-rw-r--r--src/com/android/tv/ui/InputBannerView.java3
-rw-r--r--src/com/android/tv/ui/KeypadChannelSwitchView.java4
-rw-r--r--src/com/android/tv/ui/SetupView.java419
-rw-r--r--src/com/android/tv/ui/TunableTvView.java92
-rw-r--r--src/com/android/tv/ui/TvOverlayManager.java381
-rw-r--r--src/com/android/tv/ui/sidepanel/AboutFragment.java187
-rw-r--r--src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java104
-rw-r--r--src/com/android/tv/ui/sidepanel/DeveloperFragment.java40
-rw-r--r--src/com/android/tv/ui/sidepanel/Item.java2
-rw-r--r--src/com/android/tv/ui/sidepanel/SettingsFragment.java185
-rw-r--r--src/com/android/tv/ui/sidepanel/SideFragmentManager.java12
-rw-r--r--src/com/android/tv/ui/sidepanel/SimpleItem.java34
-rw-r--r--src/com/android/tv/util/BitmapUtils.java9
-rw-r--r--src/com/android/tv/util/CollectionUtils.java41
-rw-r--r--src/com/android/tv/util/EngOnlyFeature.java41
-rw-r--r--src/com/android/tv/util/ImageCache.java1
-rw-r--r--src/com/android/tv/util/ImageLoader.java134
-rw-r--r--src/com/android/tv/util/MultiLongSparseArray.java2
-rw-r--r--src/com/android/tv/util/OnboardingUtils.java45
-rw-r--r--src/com/android/tv/util/PipInputManager.java1
-rw-r--r--src/com/android/tv/util/RecurringRunner.java12
-rw-r--r--src/com/android/tv/util/SearchManagerHelper.java2
-rw-r--r--src/com/android/tv/util/SetupUtils.java114
-rw-r--r--src/com/android/tv/util/SoftPreconditions.java6
-rw-r--r--src/com/android/tv/util/SystemProperties.java7
-rw-r--r--src/com/android/tv/util/TvSettings.java3
-rw-r--r--src/com/android/tv/util/Utils.java33
96 files changed, 4059 insertions, 3154 deletions
diff --git a/src/com/android/tv/ApplicationSingletons.java b/src/com/android/tv/ApplicationSingletons.java
index 0ef61e72..5198f7fd 100644
--- a/src/com/android/tv/ApplicationSingletons.java
+++ b/src/com/android/tv/ApplicationSingletons.java
@@ -17,7 +17,6 @@
package com.android.tv;
import com.android.tv.analytics.Analytics;
-import com.android.tv.analytics.OptOutPreferenceHelper;
import com.android.tv.analytics.Tracker;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.ProgramDataManager;
@@ -41,11 +40,11 @@ public interface ApplicationSingletons {
DvrSessionManager getDvrSessionManger();
- OptOutPreferenceHelper getOptPreferenceHelper();
-
ProgramDataManager getProgramDataManager();
Tracker getTracker();
TvInputManagerHelper getTvInputManagerHelper();
+
+ MainActivityWrapper getMainActivityWrapper();
}
diff --git a/src/com/android/tv/ChannelTuner.java b/src/com/android/tv/ChannelTuner.java
index 0195249b..0a000e9b 100644
--- a/src/com/android/tv/ChannelTuner.java
+++ b/src/com/android/tv/ChannelTuner.java
@@ -20,12 +20,13 @@ import android.media.tv.TvContract;
import android.media.tv.TvInputInfo;
import android.net.Uri;
import android.os.Handler;
+import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.util.Log;
+import com.android.tv.common.CollectionUtils;
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 com.android.tv.util.TvInputManagerHelper;
@@ -40,6 +41,7 @@ import java.util.Set;
* It manages the current tuned channel among browsable channels. And it determines the next channel
* by channel up/down. But, it doesn't actually tune through TvView.
*/
+@MainThread
public class ChannelTuner {
private static final String TAG = "ChannelTuner";
diff --git a/src/com/android/tv/Features.java b/src/com/android/tv/Features.java
index 9564afaa..1a665506 100644
--- a/src/com/android/tv/Features.java
+++ b/src/com/android/tv/Features.java
@@ -16,21 +16,21 @@
package com.android.tv;
-import static com.android.tv.common.feature.FeatureUtils.AND;
-import static com.android.tv.common.feature.FeatureUtils.OFF;
-import static com.android.tv.common.feature.FeatureUtils.ON;
-import static com.android.tv.common.feature.FeatureUtils.OR;
-import static com.android.tv.common.feature.TestableFeature.createTestableFeature;
-import static com.android.tv.util.EngOnlyFeature.ENG_ONLY_FEATURE;
-
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.PackageVersionFeature;
import com.android.tv.common.feature.PropertyFeature;
import com.android.tv.common.feature.SharedPreferencesFeature;
import com.android.tv.common.feature.TestableFeature;
+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 static com.android.tv.common.feature.TestableFeature.createTestableFeature;
+import static com.android.tv.common.feature.EngOnlyFeature.ENG_ONLY_FEATURE;
+
/**
* List of {@link Feature} for the Live TV App.
*
@@ -38,49 +38,48 @@ import com.android.tv.common.feature.TestableFeature;
*/
public final class Features {
/**
- * UI for opting out of analytics.
+ * UI for opting in to analytics.
*
- * <p>See <a href="http://b/20228119">b/20228119</a>
+ * <p>Do not turn this on until the splash screen asking existing users to opt-in is launched.
+ * See <a href="http://b/20228119">b/20228119</a>
*/
- public static Feature ANALYTICS_OPT_OUT = ENG_ONLY_FEATURE;
+ public static Feature ANALYTICS_OPT_IN = ENG_ONLY_FEATURE;
/**
* 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);
-
- /**
- * DVR
- *
- * <p>See <a href="https://goto.google.com/atv-dvr-onepager">go/atv-dvr-onepager</a>
- * <p>Note: To make DVR work, DvrTvInputService.FEATURE_DVR should be {@code true}.
- */
- public static TestableFeature DVR = createTestableFeature(OFF);
+ public static Feature ANALYTICS_V2 = AND(ON, ANALYTICS_OPT_IN);
public static Feature EPG_SEARCH = new PropertyFeature("feature_tv_use_epg_search", false);
- public static SharedPreferencesFeature USB_TUNER = new SharedPreferencesFeature("usb_tuner",
- false, OR(ENG_ONLY_FEATURE, new GServiceFeature("usbtuner_enabled", false)));
+ public static SharedPreferencesFeature USB_TUNER = new SharedPreferencesFeature(
+ "usb_tuner", true,
+ OR(ENG_ONLY_FEATURE, new GServiceFeature("usbtuner_enabled", false)));
public static Feature DEVELOPER_OPTION = OR(ENG_ONLY_FEATURE,
new GServiceFeature("usbtuner_enabled", false));
- /**
- * A flag which indicates that LC app is unhidden even when there is no input.
- */
- public static Feature UNHIDE = OFF;
+ private static final String PLAY_STORE_PACKAGE_NAME = "com.android.vending";
+ private static final int PLAY_STORE_ZIMA_VERSION_CODE = 80441186;
+ private static Feature PLAY_STORE_LINK = new PackageVersionFeature(PLAY_STORE_PACKAGE_NAME,
+ PLAY_STORE_ZIMA_VERSION_CODE);
+
+ public static Feature ONBOARDING_PLAY_STORE = PLAY_STORE_LINK;
/**
* 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);
+ public static Feature ONBOARDING_EXPERIENCE = ONBOARDING_PLAY_STORE;
- public static Feature ONBOARDING_PLAY_STORE = AND(ONBOARDING_EXPERIENCE, OFF);
- public static Feature ONBOARDING_USB_TUNER = AND(ONBOARDING_EXPERIENCE, USB_TUNER);
+ private static final String GSERVICE_KEY_UNHIDE = "live_channels_unhide";
+ /**
+ * A flag which indicates that LC app is unhidden even when there is no input.
+ */
+ public static Feature UNHIDE = AND(ONBOARDING_EXPERIENCE,
+ new GServiceFeature(GSERVICE_KEY_UNHIDE, false));
@VisibleForTesting
public static Feature TEST_FEATURE = new PropertyFeature("test_feature", false);
diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java
index 6bb2995b..5ea23a79 100644
--- a/src/com/android/tv/MainActivity.java
+++ b/src/com/android/tv/MainActivity.java
@@ -17,7 +17,6 @@
package com.android.tv;
import android.app.Activity;
-import android.app.FragmentTransaction;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -68,11 +67,15 @@ import android.widget.FrameLayout;
import android.widget.Toast;
import com.android.tv.analytics.DurationTimer;
+import com.android.tv.analytics.SendChannelStatusRunnable;
import com.android.tv.analytics.SendConfigInfoRunnable;
import com.android.tv.analytics.Tracker;
+import com.android.tv.common.BuildConfig;
+import com.android.tv.common.MemoryManageable;
import com.android.tv.common.TvCommonUtils;
+import com.android.tv.common.TvContentRatingCache;
import com.android.tv.common.WeakHandler;
-import com.android.tv.common.dvr.DvrSessionClient;
+import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.OnCurrentProgramUpdatedListener;
@@ -82,7 +85,10 @@ 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.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrPlayActivity;
+import com.android.tv.dvr.Recording;
import com.android.tv.menu.Menu;
import com.android.tv.onboarding.OnboardingActivity;
import com.android.tv.parental.ContentRatingsManager;
@@ -101,17 +107,16 @@ import com.android.tv.ui.TunableTvView;
import com.android.tv.ui.TunableTvView.OnTuneListener;
import com.android.tv.ui.TvOverlayManager;
import com.android.tv.ui.TvViewUiManager;
-import com.android.tv.ui.sidepanel.ChannelSourcesFragment;
import com.android.tv.ui.sidepanel.ClosedCaptionFragment;
import com.android.tv.ui.sidepanel.CustomizeChannelListFragment;
import com.android.tv.ui.sidepanel.DebugOptionFragment;
import com.android.tv.ui.sidepanel.DisplayModeFragment;
import com.android.tv.ui.sidepanel.MultiAudioFragment;
+import com.android.tv.ui.sidepanel.SettingsFragment;
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.ImageLoader;
import com.android.tv.util.OnboardingUtils;
import com.android.tv.util.PermissionUtils;
import com.android.tv.util.PipInputManager;
@@ -230,18 +235,17 @@ 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;
private final TvOptionsManager mTvOptionsManager = new TvOptionsManager(this);
private TvViewUiManager mTvViewUiManager;
private TimeShiftManager mTimeShiftManager;
- private DvrManager mDvrManager;
private Tracker mTracker;
private final DurationTimer mMainDurationTimer = new DurationTimer();
private final DurationTimer mTuneDurationTimer = new DurationTimer();
- private DvrSessionClient mDvrSessionClientForDebug;
+ private DvrManager mDvrManager;
+ private DvrDataManager mDvrDataManager;
private TunableTvView mTvView;
private TunableTvView mPipView;
@@ -264,7 +268,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private int mNowPlayingCardHeight;
private String mInputIdUnderSetup;
- private boolean mIsSetupActivityCalledByDialog;
+ private boolean mIsSetupActivityCalledByPopup;
private AudioManager mAudioManager;
private int mAudioFocusStatus;
private boolean mTunePending;
@@ -282,6 +286,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private boolean mNeedShowBackKeyGuide;
private boolean mVisibleBehind;
private boolean mAc3PassthroughSupported;
+ private boolean mShowNewSourcesFragment = true;
+ private Uri mRecordingUri;
private boolean mIsFilmModeSet;
private float mDefaultRefreshRate;
@@ -310,6 +316,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
private RecurringRunner mSendConfigInfoRecurringRunner;
+ private RecurringRunner mChannelStatusRecurringRunner;
// A caller which started this activity. (e.g. TvSearch)
private String mSource;
@@ -373,7 +380,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
new ChannelTuner.Listener() {
@Override
public void onLoadFinished() {
- markNewChannelsBrowsable();
+ SetupUtils.getInstance(MainActivity.this).markNewChannelsBrowsable();
if (mActivityResumed) {
resumeTvIfNeeded();
resumePipIfNeeded();
@@ -416,15 +423,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
.isParentalControlsEnabled();
mTvView.onParentalControlChanged(parentalControlEnabled);
mPipView.onParentalControlChanged(parentalControlEnabled);
- mTvOptionsManager.onParentalControlChanged(parentalControlEnabled);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG,"onCreate()");
super.onCreate(savedInstanceState);
- TvApplication tvApplication = (TvApplication) getApplication();
- tvApplication.setMainActivity(this);
if (Features.ONBOARDING_EXPERIENCE.isEnabled(this)
&& OnboardingUtils.needToShowOnboarding(this)
@@ -436,6 +440,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return;
}
+ TvApplication tvApplication = (TvApplication) getApplication();
+ tvApplication.getMainActivityWrapper().onMainActivityCreated(this);
if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) {
Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show();
}
@@ -453,7 +459,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mPipInputManager.start();
mMemoryManageables.add(mProgramDataManager);
mMemoryManageables.add(ImageCache.getInstance());
- mDvrManager = tvApplication.getDvrManager();
+ mMemoryManageables.add(TvContentRatingCache.getInstance());
+ if(CommonFeatures.DVR.isEnabled(this)) {
+ mDvrManager = tvApplication.getDvrManager();
+ mDvrDataManager = tvApplication.getDvrDataManager();
+ }
DisplayManager displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);
Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
@@ -518,9 +528,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
shrunkenTvViewHeight);
if (!PermissionUtils.hasAccessWatchedHistory(this)) {
- mWatchedHistoryManager = new WatchedHistoryManager(getApplicationContext());
- mWatchedHistoryManager.start();
- mTvView.setWatchedHistoryManager(mWatchedHistoryManager);
+ WatchedHistoryManager watchedHistoryManager = new WatchedHistoryManager(
+ getApplicationContext());
+ watchedHistoryManager.start();
+ mTvView.setWatchedHistoryManager(watchedHistoryManager);
}
mTvViewUiManager = new TvViewUiManager(this, mTvView, mPipView,
(FrameLayout) findViewById(android.R.id.content), mTvOptionsManager);
@@ -564,7 +575,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
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);
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
}
});
mSearchFragment = new ProgramGuideSearchFragment();
@@ -607,8 +619,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mAccessibilityManager =
(AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
mSendConfigInfoRecurringRunner = new RecurringRunner(this, TimeUnit.DAYS.toMillis(1),
- new SendConfigInfoRunnable(mTracker, mTvInputManagerHelper));
+ new SendConfigInfoRunnable(mTracker, mTvInputManagerHelper), null);
mSendConfigInfoRecurringRunner.start();
+ mChannelStatusRecurringRunner = SendChannelStatusRunnable
+ .startChannelStatusRecurringRunner(this, mTracker, mChannelDataManager);
initForTest();
}
@@ -668,6 +682,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
return TunableTvView.BLOCK_SCREEN_TYPE_NO_UI;
}
+ if (mOverlayManager.isSetupFragmentActive()
+ || mOverlayManager.isNewSourcesFragmentActive()) {
+ return TunableTvView.BLOCK_SCREEN_TYPE_NO_UI;
+ }
return TunableTvView.BLOCK_SCREEN_TYPE_NORMAL;
}
@@ -720,6 +738,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
SystemProperties.updateSystemProperties();
mNeedShowBackKeyGuide = true;
mActivityResumed = true;
+ mShowNewSourcesFragment = true;
int result = mAudioManager.requestAudioFocus(MainActivity.this,
AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
mAudioFocusStatus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ?
@@ -732,7 +751,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
requestVisibleBehind(true);
}
if (mChannelTuner.areAllChannelsLoaded()) {
- markNewChannelsBrowsable();
+ SetupUtils.getInstance(this).markNewChannelsBrowsable();
resumeTvIfNeeded();
resumePipIfNeeded();
}
@@ -862,32 +881,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
- private void markNewChannelsBrowsable() {
- SetupUtils setupUtils = SetupUtils.getInstance(MainActivity.this);
- Set<String> newInputsWithChannels = new HashSet<>();
- for (TvInputInfo input : mTvInputManagerHelper.getTvInputInfos(true, true)) {
- String inputId = input.getId();
- if (!setupUtils.isSetupDone(inputId)
- && mChannelDataManager.getChannelCountForInput(inputId) > 0) {
- setupUtils.onSetupDone(inputId);
- newInputsWithChannels.add(inputId);
- if (DEBUG) {
- Log.d(TAG, "New input " + inputId + " has "
- + mChannelDataManager.getChannelCountForInput(inputId)
- + " channels");
- }
- }
- }
- if (!newInputsWithChannels.isEmpty()) {
- for (Channel channel : mChannelDataManager.getChannelList()) {
- if (newInputsWithChannels.contains(channel.getInputId())) {
- mChannelDataManager.updateBrowsable(channel.getId(), true);
- }
- }
- mChannelDataManager.applyUpdatedValuesToDb();
- }
- }
-
private void startTv(Uri channelUri) {
if (DEBUG) Log.d(TAG, "startTv Uri=" + channelUri);
if ((channelUri == null || !TvContract.isChannelUriForPassthroughInput(channelUri))
@@ -945,7 +938,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mTvView.start(mTvInputManagerHelper);
setVolumeByAudioFocusStatus();
- tune();
+ if (mRecordingUri != null) {
+ playRecording(mRecordingUri);
+ mRecordingUri = null;
+ } else {
+ tune();
+ }
}
@Override
@@ -971,9 +969,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
/**
* Starts setup activity for the given input {@code input}.
*
- * @param calledByDialog If true, startSetupActivity is invoked from the setup dialog.
+ * @param calledByPopup If true, startSetupActivity is invoked from the setup fragment.
*/
- public void startSetupActivity(TvInputInfo input, boolean calledByDialog) {
+ public void startSetupActivity(TvInputInfo input, boolean calledByPopup) {
Intent intent = TvCommonUtils.createSetupIntent(input);
if (intent == null) {
Toast.makeText(this, R.string.msg_no_setup_activity, Toast.LENGTH_SHORT).show();
@@ -988,7 +986,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
SetupUtils.grantEpgPermission(this, input.getServiceInfo().packageName);
mInputIdUnderSetup = input.getId();
- mIsSetupActivityCalledByDialog = calledByDialog;
+ mIsSetupActivityCalledByPopup = calledByPopup;
// 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
@@ -1001,9 +999,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
input.loadLabel(this)), Toast.LENGTH_SHORT).show();
return;
}
- if (calledByDialog) {
+ if (calledByPopup) {
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION
- | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG);
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
} else {
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY);
@@ -1127,31 +1125,20 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
/**
- * Show channel sources fragment.
+ * Show settings fragment.
*/
- public void showChannelSourcesFragment() {
+ public void showSettingsFragment() {
if (!mChannelTuner.areAllChannelsLoaded()) {
// Show ChannelSourcesFragment only if all the channels are loaded.
return;
}
Channel currentChannel = mChannelTuner.getCurrentChannel();
long channelId = currentChannel == null ? Channel.INVALID_ID : currentChannel.getId();
- mOverlayManager.getSideFragmentManager().show(new ChannelSourcesFragment(channelId));
+ mOverlayManager.getSideFragmentManager().show(new SettingsFragment(channelId));
}
- // TODO: Refactor this.
- public void showParentalControlFragment() {
- mOverlayManager.showDialogFragment(PinDialogFragment.DIALOG_TAG,
- new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_ENTER_PIN,
- new PinDialogFragment.ResultListener() {
- @Override
- public void done(boolean success) {
- if (success) {
- mOverlayManager.getSideFragmentManager()
- .show(new ParentalControlsFragment());
- }
- }
- }), false);
+ public void showMerchantCollection() {
+ startActivitySafe(OnboardingUtils.PLAY_STORE_INTENT);
}
/**
@@ -1259,7 +1246,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
} else {
mInputIdUnderSetup = null;
}
- if (!mIsSetupActivityCalledByDialog) {
+ if (!mIsSetupActivityCalledByPopup) {
mOverlayManager.getSideFragmentManager().showSidePanel(false);
}
break;
@@ -1377,6 +1364,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
+ if (CommonFeatures.DVR.isEnabled(this)) {
+ mRecordingUri = intent.getParcelableExtra(Utils.EXTRA_KEY_RECORDING_URI);
+ if (mRecordingUri != null) {
+ return true;
+ }
+ }
+
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
try {
@@ -1502,6 +1496,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mMediaSession.setActive(false);
}
}
+ TvApplication.getSingletons(this).getMainActivityWrapper()
+ .notifyCurrentChannelChange(this, null);
mChannelTuner.resetCurrentChannel();
mTunePending = false;
}
@@ -1613,6 +1609,30 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
+ private void playRecording(Uri recordingUri) {
+ String inputId = recordingUri.getQueryParameter(Recording.PARAM_INPUT_ID);
+ SoftPreconditions.checkNotNull(inputId);
+ mTvView.playRecording(inputId, recordingUri, new OnTuneListener() {
+ @Override
+ public void onTuneFailed(Channel channel) { }
+
+ @Override
+ public void onUnexpectedStop(Channel channel) { }
+
+ @Override
+ public void onStreamInfoChanged(StreamInfo info) { }
+
+ @Override
+ public void onChannelRetuned(Uri channel) { }
+
+ @Override
+ public void onContentBlocked() { }
+
+ @Override
+ public void onContentAllowed() { }
+ });
+ }
+
private void tune() {
if (DEBUG) Log.d(TAG, "tune()");
mTuneDurationTimer.start();
@@ -1641,11 +1661,27 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
if (mChannelDataManager.getChannelCount() > 0) {
mOverlayManager.showIntroDialog();
- } else {
- mOverlayManager.showSetupDialog();
+ } else if (!Features.ONBOARDING_EXPERIENCE.isEnabled(this)) {
+ mOverlayManager.showSetupFragment();
return;
}
}
+ if (!TvCommonUtils.isRunningInTest() && mShowNewSourcesFragment
+ && setupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) {
+ // Show new channel sources fragment.
+ runAfterAttachedToWindow(new Runnable() {
+ @Override
+ public void run() {
+ mOverlayManager.runAfterOverlaysAreClosed(new Runnable() {
+ @Override
+ public void run() {
+ mOverlayManager.showNewSourcesFragment();
+ }
+ });
+ }
+ });
+ }
+ mShowNewSourcesFragment = false;
if (!mChannelTuner.isCurrentChannelPassthrough()
&& mChannelTuner.getBrowsableChannelCount() == 0
&& mChannelDataManager.getChannelCount() > 0
@@ -1656,7 +1692,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (mTvInputManagerHelper.getTunerTvInputSize() == 1) {
mOverlayManager.getSideFragmentManager().show(new CustomizeChannelListFragment());
} else {
- showChannelSourcesFragment();
+ showSettingsFragment();
}
return;
}
@@ -1673,7 +1709,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (mOverlayManager.getSideFragmentManager().isActive()) {
return;
}
- mOverlayManager.showSetupDialog();
+ mOverlayManager.showSetupFragment();
return;
}
@@ -1828,6 +1864,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
addToRecentChannels(channel.getId());
}
Utils.setLastWatchedChannel(this, channel);
+ TvApplication.getSingletons(this).getMainActivityWrapper()
+ .notifyCurrentChannelChange(this, channel);
}
checkChannelLockNeeded(mTvView);
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE);
@@ -1840,6 +1878,23 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
updateMediaSession();
}
+ private void runAfterAttachedToWindow(final Runnable runnable) {
+ if (mOverlayRootView.isLaidOut()) {
+ runnable.run();
+ } else {
+ mOverlayRootView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ mOverlayRootView.removeOnAttachStateChangeListener(this);
+ runnable.run();
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) { }
+ });
+ }
+ }
+
private void updateMediaSession() {
if (getCurrentChannel() == null) {
mMediaSession.setActive(false);
@@ -1858,7 +1913,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return;
}
- Program program = getCurrentProgram();
+ final Program program = getCurrentProgram();
String cardTitleText = program == null ? null : program.getTitle();
if (TextUtils.isEmpty(cardTitleText)) {
cardTitleText = getCurrentChannel().getDisplayName();
@@ -1868,24 +1923,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (program != null && program.getPosterArtUri() != null) {
program.loadPosterArt(MainActivity.this, mNowPlayingCardWidth, mNowPlayingCardHeight,
- new Program.LoadPosterArtCallback() {
- @Override
- public void onLoadPosterArtFinished(Program program, Bitmap posterArt) {
- if (program != getCurrentProgram() || getCurrentChannel() == null) {
- return;
- }
-
- if (posterArt != null) {
- String cardTitleText = program == null ? null : program.getTitle();
- if (TextUtils.isEmpty(cardTitleText)) {
- cardTitleText = getCurrentChannel().getDisplayName();
- }
- updateMediaMetadata(cardTitleText, posterArt);
- } else {
- updateMediaMetadataWithAlternativeArt(program);
- }
- }
- });
+ createProgramPosterArtCallback(MainActivity.this, program));
} else {
updateMediaMetadataWithAlternativeArt(program);
}
@@ -1893,6 +1931,32 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mMediaSession.setActive(true);
}
+ private static ImageLoader.ImageLoaderCallback<MainActivity> createProgramPosterArtCallback(
+ MainActivity mainActivity, final Program program) {
+ return new ImageLoader.ImageLoaderCallback<MainActivity>(mainActivity) {
+ @Override
+ public void onBitmapLoaded(MainActivity mainActivity, @Nullable Bitmap posterArt) {
+ if (program != mainActivity.getCurrentProgram()
+ || mainActivity.getCurrentChannel() == null) {
+ return;
+ }
+ mainActivity.updateProgramPosterArt(program, posterArt);
+ }
+ };
+ }
+
+ private void updateProgramPosterArt(Program program, @Nullable Bitmap posterArt) {
+ if (posterArt != null) {
+ String cardTitleText = program == null ? null : program.getTitle();
+ if (TextUtils.isEmpty(cardTitleText)) {
+ cardTitleText = getCurrentChannel().getDisplayName();
+ }
+ updateMediaMetadata(cardTitleText, posterArt);
+ } else {
+ updateMediaMetadataWithAlternativeArt(program);
+ }
+ }
+
private void updateMediaMetadata(String title, Bitmap posterArt) {
MediaMetadata.Builder builder = new MediaMetadata.Builder();
builder.putString(MediaMetadata.METADATA_KEY_TITLE, title);
@@ -2027,7 +2091,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
boolean noOverlayUiWhenResume =
mInputToSetUp == null && !mShowProgramGuide && !mShowSelectInputView;
if (needToShowBanner && noOverlayUiWhenResume
- && mOverlayManager.getCurrentDialog() == null) {
+ && mOverlayManager.getCurrentDialog() == null
+ && !mOverlayManager.isSetupFragmentActive()
+ && !mOverlayManager.isNewSourcesFragmentActive()) {
if (mChannelTuner.getCurrentChannel() == null) {
mChannelBannerHiddenBySideFragment = false;
} else if (mOverlayManager.getSideFragmentManager().isActive()) {
@@ -2047,8 +2113,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE);
}
- public boolean needToKeepDialogWhenHidingOverlay() {
- return mInputIdUnderSetup != null && mIsSetupActivityCalledByDialog;
+ public boolean needToKeepSetupScreenWhenHidingOverlay() {
+ return mInputIdUnderSetup != null && mIsSetupActivityCalledByPopup;
}
// For now, this only takes care of 24fps.
@@ -2180,10 +2246,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
public void showProgramGuideSearchFragment() {
- FragmentTransaction ft = getFragmentManager().beginTransaction();
- ft.replace(R.id.search, mSearchFragment);
- ft.addToBackStack(null);
- ft.commit();
+ getFragmentManager().beginTransaction().replace(R.id.fragment_container, mSearchFragment)
+ .addToBackStack(null).commit();
}
@Override
@@ -2204,7 +2268,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (mProgramDataManager != null) {
mProgramDataManager.removeOnCurrentProgramUpdatedListener(
Channel.INVALID_ID, mOnCurrentProgramUpdatedListener);
- if (application.isCurrentMainActivity(this)) {
+ if (application.getMainActivityWrapper().isCurrent(this)) {
mProgramDataManager.setPrefetchEnabled(false);
}
}
@@ -2225,13 +2289,15 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mAudioCapabilitiesReceiver.unregister();
}
mHandler.removeCallbacksAndMessages(null);
- if (application.isCurrentMainActivity(this)) {
- application.setMainActivity(null);
- }
+ application.getMainActivityWrapper().onMainActivityDestroyed(this);
if (mSendConfigInfoRecurringRunner != null) {
mSendConfigInfoRecurringRunner.stop();
mSendConfigInfoRecurringRunner = null;
}
+ if (mChannelStatusRecurringRunner != null) {
+ mChannelStatusRecurringRunner.stop();
+ mChannelStatusRecurringRunner = null;
+ }
super.onDestroy();
}
@@ -2295,11 +2361,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
* I KEYCODE_TV_INPUT
* O debug: show display mode option
* P debug: togglePipView
- * R KEYCODE_MEDIA_STOP debug: dvr stop recording
* S KEYCODE_CAPTIONS: select subtitle
- * V KEYCODE_MEDIA_RECORD debug: dvr start recording
* W debug: toggle screen size
- * Z KEYCODE_PROG_RED debug: create program data for current channel
+ * V KEYCODE_MEDIA_RECORD debug: record the current channel for 30 sec
+ * X KEYCODE_BUTTON_X KEYCODE_PROG_BLUE debug: record current channel for a few minutes
+ * Y KEYCODE_BUTTON_Y KEYCODE_PROG_GREEN debug: Play a recording
*/
if (SystemProperties.LOG_KEYEVENT.getValue()) {
Log.d(TAG, "onKeyUp(" + keyCode + ", " + event + ")");
@@ -2348,7 +2414,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_E:
case KeyEvent.KEYCODE_MENU:
- showChannelSourcesFragment();
+ showSettingsFragment();
return true;
}
} else {
@@ -2487,51 +2553,48 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
case KeyEvent.KEYCODE_MEDIA_RECORD: // TODO(DVR) handle with debug_keys set
case KeyEvent.KEYCODE_V: {
- if (mDvrSessionClientForDebug != null) {
- mDvrSessionClientForDebug.release();
- mDvrSessionClientForDebug = null;
- }
- mDvrSessionClientForDebug = new DvrSessionClient(MainActivity.this);
- Channel dvrChannel = null;
- for (Channel channel : mChannelDataManager.getBrowsableChannelList()) {
- if (channel.getInputId().equals(DVR_TEST_INPUT_ID)) {
- dvrChannel = channel;
- break;
+ DvrManager dvrManager = TvApplication.getSingletons(this).getDvrManager();
+ long startTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(5);
+ long endTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(35);
+ dvrManager.addSchedule(getCurrentChannel(), startTime, endTime);
+ return true;
+ }
+ case KeyEvent.KEYCODE_PROG_BLUE:
+ case KeyEvent.KEYCODE_BUTTON_X:
+ case KeyEvent.KEYCODE_X: {
+ if (CommonFeatures.DVR.isEnabled(this)) {
+ Channel channel = mTvView.getCurrentChannel();
+ long channelId = channel.getId();
+ Program p = mProgramDataManager.getCurrentProgram(channelId);
+ if (p == null) {
+ long now = System.currentTimeMillis();
+ mDvrManager
+ .addSchedule(channel, now, now + TimeUnit.MINUTES.toMillis(1));
+ } else {
+ mDvrManager.addSchedule(p,
+ mDvrManager.getScheduledRecordingsThatConflict(p));
}
- }
- if (dvrChannel == null) {
return true;
}
- final Channel channel = dvrChannel;
- mDvrSessionClientForDebug.connect(DVR_TEST_INPUT_ID, new DvrSessionClient.Callback() {
- @Override
- public void onConnected() {
- mDvrSessionClientForDebug.startRecord(channel.getUri(), channel.getUri());
- }
- });
- return true;
}
- case KeyEvent.KEYCODE_MEDIA_STOP: // TODO(DVR) handle with debug_keys set
- case KeyEvent.KEYCODE_R:
- if (mDvrSessionClientForDebug == null) {
+ case KeyEvent.KEYCODE_PROG_YELLOW:
+ case KeyEvent.KEYCODE_BUTTON_Y:
+ case KeyEvent.KEYCODE_Y: {
+ if (CommonFeatures.DVR.isEnabled(this)) {
+ // TODO(DVR) only get finished recordings.
+ List<Recording> recordings = mDvrDataManager.getRecordings();
+ Log.d(TAG, "Found " + recordings.size() + " recordings");
+ if (recordings.isEmpty()) {
+ Toast.makeText(this, "No finished recording to play", Toast.LENGTH_LONG)
+ .show();
+ } else {
+ Recording r = recordings.get(0);
+ Intent intent = new Intent(this, DvrPlayActivity.class);
+ intent.putExtra(Recording.RECORDING_ID_EXTRA, r.getId());
+ startActivity(intent);
+ }
return true;
}
- mDvrSessionClientForDebug.stopRecord();
- mDvrSessionClientForDebug.release();
- mDvrSessionClientForDebug = null;
- return true;
- case KeyEvent.KEYCODE_PROG_RED:
- case KeyEvent.KEYCODE_Z: {
- Channel channel = mTvView.getCurrentChannel();
- long channelId = channel.getId();
- Program p = mProgramDataManager.getCurrentProgram(channelId);
- if (p == null) {
- long now = System.currentTimeMillis();
- mDvrManager.addSchedule(channel, now, now + TimeUnit.MINUTES.toMillis(5));
- } else {
- mDvrManager.addSchedule(p);
- }
- return true;
}
}
}
@@ -2657,7 +2720,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
tuneToChannel(mChannelTuner.getCurrentChannel());
} else {
- showChannelSourcesFragment();
+ showSettingsFragment();
}
}
@@ -2722,7 +2785,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
// Channel banner would be updated inside of tune.
tune();
} else {
- showChannelSourcesFragment();
+ showSettingsFragment();
}
}
}
@@ -2730,7 +2793,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
/**
* This method just moves the channel in the channel map and updates the channel banner,
* but doesn't actually tune to the channel.
- * The caller of this method should call tune() in the end.
+ * The caller of this method should call {@link #tune} in the end.
*
* @param channelUp {@code true} for channel up, and {@code false} for channel down.
* @param fastTuning {@code true} if fast tuning is requested.
diff --git a/src/com/android/tv/MainActivityWrapper.java b/src/com/android/tv/MainActivityWrapper.java
new file mode 100644
index 00000000..94f11864
--- /dev/null
+++ b/src/com/android/tv/MainActivityWrapper.java
@@ -0,0 +1,129 @@
+/*
+ * 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.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+
+import com.android.tv.common.CollectionUtils;
+import com.android.tv.data.Channel;
+
+import java.util.Set;
+
+/**
+ * A wrapper for safely getting the current {@link MainActivity}.
+ * Note that this class is not thread-safe. All the public methods should be called on main thread.
+ */
+@MainThread
+public final class MainActivityWrapper {
+ private MainActivity mActivity;
+
+ private final Set<OnCurrentChannelChangeListener> mListeners = CollectionUtils.createSmallSet();
+
+ /**
+ * Returns the current main activity.
+ * <b>WARNING</b> do not keep a reference to MainActivity, leaking activities is expensive.
+ */
+ MainActivity getMainActivity() {
+ return mActivity;
+ }
+
+ /**
+ * Checks if the given {@code activity} is the current main activity.
+ */
+ boolean isCurrent(MainActivity activity) {
+ return activity != null && mActivity == activity;
+ }
+
+ /**
+ * Sets the currently created main activity instance.
+ */
+ @UiThread
+ public void onMainActivityCreated(@NonNull MainActivity activity) {
+ mActivity = activity;
+ }
+
+ /**
+ * Unsets the main activity instance.
+ */
+ @UiThread
+ public void onMainActivityDestroyed(@NonNull MainActivity activity) {
+ if (mActivity != activity) {
+ mActivity = null;
+ }
+ }
+
+ /**
+ * Notifies the current channel change.
+ */
+ void notifyCurrentChannelChange(@NonNull MainActivity caller, @Nullable Channel channel) {
+ if (mActivity == caller) {
+ for (OnCurrentChannelChangeListener listener : mListeners) {
+ listener.onCurrentChannelChange(channel);
+ }
+ }
+ }
+
+ /**
+ * Checks if the main activity is created.
+ */
+ public boolean isCreated() {
+ return mActivity != null;
+ }
+
+ /**
+ * Checks if the main activity is started.
+ */
+ public boolean isStarted() {
+ return mActivity != null && mActivity.isActivityStarted();
+ }
+
+ /**
+ * Checks if the main activity is resumed.
+ */
+ public boolean isResumed() {
+ return mActivity != null && mActivity.isActivityResumed();
+ }
+
+ /**
+ * Adds OnCurrentChannelChangeListener.
+ */
+ @UiThread
+ public void addOnCurrentChannelChangeListener(OnCurrentChannelChangeListener listener) {
+ mListeners.add(listener);
+ }
+
+ /**
+ * Removes OnCurrentChannelChangeListener.
+ */
+ @UiThread
+ public void removeOnCurrentChannelChangeListener(OnCurrentChannelChangeListener listener) {
+ mListeners.remove(listener);
+ }
+
+ /**
+ * Listener for the current channel change in main activity.
+ */
+ public interface OnCurrentChannelChangeListener {
+ /**
+ * Called when the current channel changes.
+ */
+ void onCurrentChannelChange(@Nullable Channel channel);
+ }
+}
diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java
index 0c7a5d65..bdabf25b 100644
--- a/src/com/android/tv/SetupPassthroughActivity.java
+++ b/src/com/android/tv/SetupPassthroughActivity.java
@@ -79,7 +79,9 @@ public class SetupPassthroughActivity extends Activity {
@Override
public void onActivityResult(int requestCode, final int resultCode, final Intent data) {
- if (requestCode != REQUEST_START_SETUP_ACTIVITY || resultCode != Activity.RESULT_OK) {
+ boolean setupComplete = requestCode == REQUEST_START_SETUP_ACTIVITY
+ && resultCode == Activity.RESULT_OK;
+ if (!setupComplete) {
setResult(resultCode, data);
finish();
return;
diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java
index 5a7b51c1..f96464e3 100644
--- a/src/com/android/tv/TimeShiftManager.java
+++ b/src/com/android/tv/TimeShiftManager.java
@@ -86,9 +86,9 @@ public class TimeShiftManager {
public static final int PLAY_DIRECTION_BACKWARD = 1;
@Retention(RetentionPolicy.SOURCE)
- @IntDef({TIME_SHIFT_ACTION_ID_PLAY, TIME_SHIFT_ACTION_ID_PAUSE, TIME_SHIFT_ACTION_ID_REWIND,
- TIME_SHIFT_ACTION_ID_FAST_FORWARD, TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS,
- TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT})
+ @IntDef(flag = true, value = {TIME_SHIFT_ACTION_ID_PLAY, TIME_SHIFT_ACTION_ID_PAUSE,
+ TIME_SHIFT_ACTION_ID_REWIND, TIME_SHIFT_ACTION_ID_FAST_FORWARD,
+ TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT})
public @interface TimeShiftActionId{}
public static final int TIME_SHIFT_ACTION_ID_PLAY = 1;
public static final int TIME_SHIFT_ACTION_ID_PAUSE = 1 << 1;
@@ -103,6 +103,7 @@ public class TimeShiftManager {
private static final long MAX_DUMMY_PROGRAM_DURATION = TimeUnit.MINUTES.toMillis(30);
@VisibleForTesting
static final long INVALID_TIME = -1;
+ static final long CURRENT_TIME = -2;
private static final long PREFETCH_TIME_OFFSET_FROM_PROGRAM_END = TimeUnit.MINUTES.toMillis(1);
private static final long PREFETCH_DURATION_FOR_NEXT = TimeUnit.HOURS.toMillis(2);
@@ -218,6 +219,22 @@ public class TimeShiftManager {
}
/**
+ * Returns the end time of the recording in milliseconds.
+ */
+ public long getRecordEndTimeMs() {
+ if (mPlayController.mRecordEndTimeMs == CURRENT_TIME) {
+ return System.currentTimeMillis();
+ } else {
+ return mPlayController.mRecordEndTimeMs;
+ }
+ }
+
+ public boolean isPlayForRecording() {
+ // TODO: need to find better way to check if it's for recording playback.
+ return mPlayController.mRecordEndTimeMs != CURRENT_TIME;
+ }
+
+ /**
* Plays the media.
*
* @throws IllegalStateException if the trick play is not available.
@@ -419,7 +436,7 @@ public class TimeShiftManager {
// Fast forward action and jump to next action
threshold = isActionEnabled(TIME_SHIFT_ACTION_ID_FAST_FORWARD)
? DISABLE_ACTION_THRESHOLD : ENABLE_ACTION_THRESHOLD;
- enabled = System.currentTimeMillis() - mCurrentPositionMediator.mCurrentPositionMs
+ enabled = getRecordEndTimeMs() - mCurrentPositionMediator.mCurrentPositionMs
> threshold;
enableAction(TIME_SHIFT_ACTION_ID_FAST_FORWARD, enabled);
enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT, enabled);
@@ -495,13 +512,14 @@ public class TimeShiftManager {
}
}
- void onRecordStartTimeChanged() {
+ void onRecordTimeRangeChanged() {
if (mPlayController.isAvailable()) {
- mProgramManager.onRecordStartTimeChanged(mPlayController.mRecordStartTimeMs);
+ mProgramManager.onRecordTimeRangeChanged(mPlayController.mRecordStartTimeMs,
+ mPlayController.mRecordEndTimeMs);
}
updateActions();
if (mNotificationEnabled && mListener != null) {
- mListener.onRecordStartTimeChanged();
+ mListener.onRecordTimeRangeChanged();
}
}
@@ -574,6 +592,7 @@ public class TimeShiftManager {
private long mPossibleStartTimeMs;
private long mRecordStartTimeMs;
+ private long mRecordEndTimeMs;
@PlayStatus private int mPlayStatus = PLAY_STATUS_PAUSED;
@PlaySpeed private int mDisplayedPlaySpeed = PLAY_SPEED_1X;
@@ -604,6 +623,7 @@ public class TimeShiftManager {
mIsPlayOffsetChanged = false;
mPossibleStartTimeMs = System.currentTimeMillis();
mRecordStartTimeMs = mPossibleStartTimeMs;
+ mRecordEndTimeMs = CURRENT_TIME;
mCurrentPositionMediator.initialize(mPossibleStartTimeMs);
mHandler.removeMessages(MSG_GET_CURRENT_POSITION);
@@ -622,7 +642,8 @@ public class TimeShiftManager {
@Override
public void onRecordStartTimeChanged(long recordStartTimeMs) {
- if (recordStartTimeMs < mPossibleStartTimeMs) {
+ if (mRecordEndTimeMs == CURRENT_TIME &&
+ recordStartTimeMs < mPossibleStartTimeMs) {
// Do not warn in this case because it can happen in normal cases.
if (DEBUG) {
Log.d(TAG, "Record start time is less then the time when it became "
@@ -637,7 +658,7 @@ public class TimeShiftManager {
return;
}
mRecordStartTimeMs = recordStartTimeMs;
- TimeShiftManager.this.onRecordStartTimeChanged();
+ TimeShiftManager.this.onRecordTimeRangeChanged();
// According to the UX guidelines, the stream should be resumed if the
// recording buffer fills up while paused, which means that the current time
@@ -654,6 +675,21 @@ public class TimeShiftManager {
TimeShiftManager.this.play();
}
}
+
+ @Override
+ public void onRecordEndTimeChanged(long recordEndTimeMs) {
+ if (mRecordEndTimeMs == recordEndTimeMs) {
+ return;
+ }
+ mRecordEndTimeMs = recordEndTimeMs;
+ TimeShiftManager.this.onRecordTimeRangeChanged();
+
+ if (mPlayStatus == PLAY_STATUS_PLAYING &&
+ mRecordEndTimeMs - getCurrentPositionMs()
+ < RECORDING_BOUNDARY_THRESHOLD) {
+ TimeShiftManager.this.pause();
+ }
+ }
});
}
@@ -663,7 +699,8 @@ public class TimeShiftManager {
void handleGetCurrentPosition() {
if (mIsPlayOffsetChanged) {
- long currentTimeMs = System.currentTimeMillis();
+ long currentTimeMs = mRecordEndTimeMs == CURRENT_TIME ? System.currentTimeMillis()
+ : mRecordEndTimeMs;
long currentPositionMs = Math.max(
Math.min(mTvView.timeshiftGetCurrentPositionMs(), currentTimeMs),
mRecordStartTimeMs);
@@ -755,8 +792,9 @@ public class TimeShiftManager {
* Moves to the specified time.
*/
void seekTo(long timeMs) {
- mTvView.timeshiftSeekTo(Math.min(System.currentTimeMillis(),
- Math.max(mRecordStartTimeMs, timeMs)));
+ mTvView.timeshiftSeekTo(Math.min(mRecordEndTimeMs == CURRENT_TIME
+ ? System.currentTimeMillis() : mRecordEndTimeMs,
+ Math.max(mRecordStartTimeMs, timeMs)));
mIsPlayOffsetChanged = true;
}
@@ -851,17 +889,19 @@ public class TimeShiftManager {
}
}
- void onRecordStartTimeChanged(long startTimeMs) {
+ void onRecordTimeRangeChanged(long startTimeMs, long endTimeMs) {
if (mChannel == null || mChannel.isPassthrough()) {
return;
}
- long currentMs = System.currentTimeMillis();
+ if (endTimeMs == CURRENT_TIME) {
+ endTimeMs = System.currentTimeMillis();
+ }
long fetchStartTimeMs = Utils.floorTime(startTimeMs, MAX_DUMMY_PROGRAM_DURATION);
boolean needToLoad = addDummyPrograms(fetchStartTimeMs,
- currentMs + PREFETCH_DURATION_FOR_NEXT);
+ endTimeMs + PREFETCH_DURATION_FOR_NEXT);
if (needToLoad) {
- Range<Long> period = Range.create(fetchStartTimeMs, currentMs);
+ Range<Long> period = Range.create(fetchStartTimeMs, endTimeMs);
mProgramLoadQueue.add(period);
startTaskIfNeeded();
}
@@ -1274,7 +1314,7 @@ public class TimeShiftManager {
/**
* Called when the recordStartTime has been changed.
*/
- void onRecordStartTimeChanged();
+ void onRecordTimeRangeChanged();
/**
* Called when the current position is changed.
diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java
index e710026f..0cac4a3b 100644
--- a/src/com/android/tv/TvApplication.java
+++ b/src/com/android/tv/TvApplication.java
@@ -26,23 +26,28 @@ import android.content.pm.PackageManager;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputManager.TvInputCallback;
-import android.os.AsyncTask;
import android.os.Bundle;
import android.os.StrictMode;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
import android.util.Log;
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.common.BuildConfig;
+import com.android.tv.common.SharedPreferencesUtils;
import com.android.tv.common.TvCommonUtils;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.ui.setup.animation.SetupAnimationHelper;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.ProgramDataManager;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManagerImpl;
+import com.android.tv.dvr.DvrDataManagerInMemoryImpl;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrRecordingService;
import com.android.tv.dvr.DvrSessionManager;
@@ -56,7 +61,6 @@ import java.util.List;
public class TvApplication extends Application implements ApplicationSingletons {
private static final String TAG = "TvApplication";
private static final boolean DEBUG = false;
- private static String versionName = "";
/**
* Returns the @{@link ApplicationSingletons} using the application context.
@@ -64,29 +68,31 @@ public class TvApplication extends Application implements ApplicationSingletons
public static ApplicationSingletons getSingletons(Context context) {
return (ApplicationSingletons) context.getApplicationContext();
}
+ private String mVersionName = "";
+
+ private final MainActivityWrapper mMainActivityWrapper = new MainActivityWrapper();
- private MainActivity mMainActivity;
private SelectInputActivity mSelectInputActivity;
private Analytics mAnalytics;
private Tracker mTracker;
private TvInputManagerHelper mTvInputManagerHelper;
private ChannelDataManager mChannelDataManager;
private ProgramDataManager mProgramDataManager;
- private OptOutPreferenceHelper mOptPreferenceHelper;
private DvrManager mDvrManager;
- private DvrDataManagerImpl mDvrDataManager;
+ private DvrDataManager mDvrDataManager;
@Nullable
private DvrSessionManager mDvrSessionManager;
@Override
public void onCreate() {
super.onCreate();
+ SharedPreferencesUtils.initialize(this);
try {
PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
- versionName = pInfo.versionName;
+ mVersionName = pInfo.versionName;
} catch (PackageManager.NameNotFoundException e) {
- Log.w(TAG, "Unable to get version name.", e);
- versionName = "";
+ Log.w(TAG, "Unable to find package '" + getPackageName() + "'.", e);
+ mVersionName = "";
}
Log.i(TAG, "Starting Live TV " + getVersionName());
// Only set StrictMode for ENG builds because the build server only produces userdebug
@@ -102,33 +108,12 @@ public class TvApplication extends Application implements ApplicationSingletons
}
StrictMode.setVmPolicy(vmPolicyBuilder.build());
}
-
if (BuildConfig.ENG && !SystemProperties.ALLOW_ANALYTICS_IN_ENG.getValue()) {
mAnalytics = StubAnalytics.getInstance(this);
} else {
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();
- }
mTvInputManagerHelper = new TvInputManagerHelper(this);
mTvInputManagerHelper.start();
mTvInputManagerHelper.addCallback(new TvInputCallback() {
@@ -142,12 +127,16 @@ public class TvApplication extends Application implements ApplicationSingletons
handleInputCountChanged();
}
});
- if (DEBUG) Log.i(TAG, "Started Live TV " + versionName);
- if (Features.DVR.isEnabled(this)) {
+ if (CommonFeatures.DVR.isEnabled(this)) {
mDvrManager = new DvrManager(this);
//NOTE: DvrRecordingService just keeps running.
DvrRecordingService.startService(this);
}
+ // In SetupFragment, transitions are set in the constructor. Because the fragment can be
+ // created in Activity.onCreate() by the framework, SetupAnimationHelper should be
+ // initialized here before Activity.onCreate() is called.
+ SetupAnimationHelper.initialize(this);
+ if (DEBUG) Log.i(TAG, "Started Live TV " + mVersionName);
}
/**
@@ -182,10 +171,6 @@ public class TvApplication extends Application implements ApplicationSingletons
return mTracker;
}
- @Override
- public OptOutPreferenceHelper getOptPreferenceHelper(){
- return mOptPreferenceHelper;
- }
/**
* Returns {@link ChannelDataManager}.
@@ -193,7 +178,7 @@ public class TvApplication extends Application implements ApplicationSingletons
@Override
public ChannelDataManager getChannelDataManager() {
if (mChannelDataManager == null) {
- mChannelDataManager = new ChannelDataManager(this, mTvInputManagerHelper, mTracker);
+ mChannelDataManager = new ChannelDataManager(this, mTvInputManagerHelper);
mChannelDataManager.start();
}
return mChannelDataManager;
@@ -217,8 +202,13 @@ public class TvApplication extends Application implements ApplicationSingletons
@Override
public DvrDataManager getDvrDataManager() {
if (mDvrDataManager == null) {
- mDvrDataManager = new DvrDataManagerImpl(this);
- mDvrDataManager.start();
+ if(SystemProperties.USE_IN_MEMORY_DVR_DB.getValue()){
+ mDvrDataManager = new DvrDataManagerInMemoryImpl(this);
+ } else {
+ DvrDataManagerImpl dvrDataManager = new DvrDataManagerImpl(this);
+ mDvrDataManager = dvrDataManager;
+ dvrDataManager.start();
+ }
}
return mDvrDataManager;
}
@@ -232,11 +222,11 @@ public class TvApplication extends Application implements ApplicationSingletons
}
/**
- * MainActivity is set in {@link MainActivity#onCreate} and cleared in
- * {@link MainActivity#onDestroy}.
+ * Returns the main activity information.
*/
- public void setMainActivity(MainActivity activity) {
- mMainActivity = activity;
+ @Override
+ public MainActivityWrapper getMainActivityWrapper() {
+ return mMainActivityWrapper;
}
/**
@@ -248,27 +238,10 @@ public class TvApplication extends Application implements ApplicationSingletons
}
/**
- * Checks if MainActivity is set or not.
- */
- public boolean hasMainActivity() {
- 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 (mMainActivity == null || !mMainActivity.isActivityResumed()) {
+ if (!mMainActivityWrapper.isResumed()) {
startMainActivity(null);
}
}
@@ -292,8 +265,8 @@ public class TvApplication extends Application implements ApplicationSingletons
if (inputCount < 2) {
return;
}
- Activity activityToHandle = mMainActivity != null && mMainActivity.isActivityResumed()
- ? mMainActivity : mSelectInputActivity;
+ Activity activityToHandle = mMainActivityWrapper.isResumed()
+ ? mMainActivityWrapper.getMainActivity() : mSelectInputActivity;
if (activityToHandle != null) {
// If startActivity is called, MainActivity.onPause is unnecessarily called. To
// prevent it, MainActivity.dispatchKeyEvent is directly called.
@@ -301,7 +274,7 @@ public class TvApplication extends Application implements ApplicationSingletons
new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TV_INPUT));
activityToHandle.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP,
KeyEvent.KEYCODE_TV_INPUT));
- } else if (mMainActivity != null && mMainActivity.isActivityStarted()) {
+ } else if (mMainActivityWrapper.isStarted()) {
Bundle extras = new Bundle();
extras.putString(Utils.EXTRA_KEY_ACTION, Utils.EXTRA_ACTION_SHOW_TV_INPUT);
startMainActivity(extras);
@@ -323,8 +296,13 @@ public class TvApplication extends Application implements ApplicationSingletons
startActivity(intent);
}
- public static String getVersionName() {
- return versionName;
+ /**
+ * Returns the version name of the live channels.
+ *
+ * @see PackageInfo#versionName
+ */
+ public String getVersionName() {
+ return mVersionName;
}
/**
@@ -333,23 +311,26 @@ public class TvApplication extends Application implements ApplicationSingletons
*/
public void handleInputCountChanged() {
TvInputManager inputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE);
- if (!Features.UNHIDE.isEnabled(TvApplication.this)) {
+ boolean enable = false;
+ if (Features.UNHIDE.isEnabled(TvApplication.this)) {
+ enable = true;
+ } else {
List<TvInputInfo> inputs = inputManager.getTvInputList();
// Enable the TvActivity 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;
}
}
- PackageManager packageManager = getPackageManager();
- ComponentName name = new ComponentName(this, TvActivity.class);
- int newState = enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
- PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
- if (packageManager.getComponentEnabledSetting(name) != newState) {
- packageManager.setComponentEnabledSetting(name, newState, 0);
- }
+ if (DEBUG) Log.d(TAG, "Enable MainActivity: " + enable);
+ }
+ PackageManager packageManager = getPackageManager();
+ ComponentName name = new ComponentName(this, TvActivity.class);
+ int newState = enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+ if (packageManager.getComponentEnabledSetting(name) != newState) {
+ packageManager.setComponentEnabledSetting(name, newState, 0);
}
SetupUtils.getInstance(TvApplication.this).onInputListUpdated(inputManager);
}
diff --git a/src/com/android/tv/TvOptionsManager.java b/src/com/android/tv/TvOptionsManager.java
index 8af35fae..97b9d5fa 100644
--- a/src/com/android/tv/TvOptionsManager.java
+++ b/src/com/android/tv/TvOptionsManager.java
@@ -37,9 +37,8 @@ public class TvOptionsManager {
public static final int OPTION_DISPLAY_MODE = 1;
public static final int OPTION_PIP = 2;
public static final int OPTION_MULTI_AUDIO = 3;
- public static final int OPTION_CHANNEL_SOURCES = 4;
- public static final int OPTION_PARENTAL_CONTROLS = 5;
- public static final int OPTION_ABOUT = 6;
+ public static final int OPTION_MORE_CHANNELS = 4;
+ public static final int OPTION_SETTINGS = 5;
public static final int OPTION_PIP_INPUT = 100;
public static final int OPTION_PIP_SWAP = 101;
@@ -54,7 +53,6 @@ public class TvOptionsManager {
private int mDisplayMode;
private boolean mPip;
private String mMultiAudio;
- private boolean mIsParentalControlEnabled;
private String mPipInput;
private boolean mPipSwap;
@PipSound private int mPipSound;
@@ -82,10 +80,6 @@ public class TvOptionsManager {
mPip ? R.string.options_item_pip_on : R.string.options_item_pip_off);
case OPTION_MULTI_AUDIO:
return mMultiAudio;
- case OPTION_PARENTAL_CONTROLS:
- return mContext.getString(
- mIsParentalControlEnabled ? R.string.option_toggle_parental_controls_on
- : R.string.option_toggle_parental_controls_off);
case OPTION_PIP_INPUT:
return mPipInput;
case OPTION_PIP_SWAP:
@@ -144,11 +138,6 @@ public class TvOptionsManager {
notifyOptionChanged(OPTION_MULTI_AUDIO);
}
- public void onParentalControlChanged(boolean isParentalControlEnabled) {
- mIsParentalControlEnabled = isParentalControlEnabled;
- notifyOptionChanged(OPTION_PARENTAL_CONTROLS);
- }
-
public void onPipInputChanged(String pipInput) {
mPipInput = pipInput;
notifyOptionChanged(OPTION_PIP_INPUT);
diff --git a/src/com/android/tv/analytics/OptOutPreferenceHelper.java b/src/com/android/tv/analytics/OptOutPreferenceHelper.java
deleted file mode 100644
index 7fefaa46..00000000
--- a/src/com/android/tv/analytics/OptOutPreferenceHelper.java
+++ /dev/null
@@ -1,112 +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.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/SendChannelStatusRunnable.java b/src/com/android/tv/analytics/SendChannelStatusRunnable.java
new file mode 100644
index 00000000..b5b5805c
--- /dev/null
+++ b/src/com/android/tv/analytics/SendChannelStatusRunnable.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.analytics;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.MainThread;
+
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.util.RecurringRunner;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Periodically sends analytics data with the channel count.
+ *
+ * <p>
+ * <p>This should only be started from a user activity
+ * like {@link com.android.tv.MainActivity}.
+ */
+@MainThread
+public class SendChannelStatusRunnable implements Runnable {
+ private static final long SEND_CHANNEL_STATUS_INTERVAL_MS = TimeUnit.DAYS.toMillis(1);
+
+ public static RecurringRunner startChannelStatusRecurringRunner(Context context,
+ Tracker tracker, ChannelDataManager channelDataManager) {
+
+ final SendChannelStatusRunnable sendChannelStatusRunnable = new SendChannelStatusRunnable(
+ channelDataManager, tracker);
+
+ Runnable onStopRunnable = new Runnable() {
+ @Override
+ public void run() {
+ sendChannelStatusRunnable.setDbLoadListener(null);
+ }
+ };
+ final RecurringRunner recurringRunner = new RecurringRunner(context,
+ SEND_CHANNEL_STATUS_INTERVAL_MS, sendChannelStatusRunnable, onStopRunnable);
+
+ if (channelDataManager.isDbLoadFinished()) {
+ sendChannelStatusRunnable.setDbLoadListener(null);
+ recurringRunner.start();
+ } else {
+ //Start the recurring runnable after the channel DB is finished loading.
+ sendChannelStatusRunnable.setDbLoadListener(new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ // This is called inside an iterator of Listeners so the remove step is done
+ // via a post on the main thread
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ sendChannelStatusRunnable.setDbLoadListener(null);
+ }
+ });
+ recurringRunner.start();
+ }
+
+ @Override
+ public void onChannelListUpdated() { }
+
+ @Override
+ public void onChannelBrowsableChanged() { }
+ });
+ }
+ return recurringRunner;
+ }
+
+ private final ChannelDataManager mChannelDataManager;
+ private final Tracker mTracker;
+ private ChannelDataManager.Listener mListener;
+
+ private SendChannelStatusRunnable(ChannelDataManager channelDataManager, Tracker tracker) {
+ mChannelDataManager = channelDataManager;
+ mTracker = tracker;
+ }
+
+ @Override
+ public void run() {
+ int browsableChannelCount = 0;
+ List<Channel> channelList = mChannelDataManager.getChannelList();
+ for (Channel channel : channelList) {
+ if (channel.isBrowsable()) {
+ ++browsableChannelCount;
+ }
+ }
+ mTracker.sendChannelCount(browsableChannelCount, channelList.size());
+ }
+
+ private void setDbLoadListener(ChannelDataManager.Listener listener) {
+ if (mListener != null) {
+ mChannelDataManager.removeListener(mListener);
+ }
+ mListener = listener;
+ if (listener != null) {
+ mChannelDataManager.addListener(listener);
+ }
+ }
+}
diff --git a/src/com/android/tv/analytics/StubAnalytics.java b/src/com/android/tv/analytics/StubAnalytics.java
index ae4cdafa..99c10d94 100644
--- a/src/com/android/tv/analytics/StubAnalytics.java
+++ b/src/com/android/tv/analytics/StubAnalytics.java
@@ -28,7 +28,7 @@ public final class StubAnalytics implements Analytics {
}
private final Tracker mTracker = new StubTracker();
- private boolean mOptOut = OptOutPreferenceHelper.ANALYTICS_OPT_OUT_DEFAULT_VALUE;
+ private boolean mOptOut = true;
private StubAnalytics(Context context) {
}
diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java
index 7411d297..ba3c59ba 100644
--- a/src/com/android/tv/data/Channel.java
+++ b/src/com/android/tv/data/Channel.java
@@ -16,11 +16,11 @@
package com.android.tv.data;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
-import android.graphics.Bitmap;
import android.media.tv.TvContract;
import android.media.tv.TvInputInfo;
import android.net.Uri;
@@ -31,6 +31,7 @@ import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
+import com.android.tv.common.CollectionUtils;
import com.android.tv.common.TvCommonConstants;
import com.android.tv.dvr.provider.DvrContract;
import com.android.tv.util.ImageLoader;
@@ -38,8 +39,6 @@ import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
@@ -90,6 +89,7 @@ public final class Channel {
};
// Additional fields added in MNC.
+ @SuppressLint("InlinedApi")
private static final String[] PROJECTION_ADDED_IN_MNC = {
// Columns should match what is read in Channel.fromCursor()
TvContract.Channels.COLUMN_APP_LINK_TEXT,
@@ -102,12 +102,8 @@ public final class Channel {
public static final String[] PROJECTION = createProjection();
private static String[] createProjection() {
- if (Build.VERSION.SDK_INT >= 23) {
- ArrayList<String> temp = new ArrayList<>(
- PROJECTION_BASE.length + PROJECTION_ADDED_IN_MNC.length);
- temp.addAll(Arrays.asList(PROJECTION_BASE));
- temp.addAll(Arrays.asList(PROJECTION_ADDED_IN_MNC));
- return temp.toArray(new String[temp.size()]);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return CollectionUtils.concatAll(PROJECTION_BASE, PROJECTION_ADDED_IN_MNC);
} else {
return PROJECTION_BASE;
}
@@ -142,7 +138,7 @@ public final class Channel {
channel.mVideoFormat = Utils.intern(cursor.getString(index++));
channel.mBrowsable = cursor.getInt(index++) == 1;
channel.mLocked = cursor.getInt(index++) == 1;
- if (Build.VERSION.SDK_INT >= 23) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
channel.mAppLinkText = cursor.getString(index++);
channel.mAppLinkColor = cursor.getInt(index++);
channel.mAppLinkIconUri = cursor.getString(index++);
@@ -190,10 +186,6 @@ public final class Channel {
*/
private boolean mRecordable;
- public interface LoadImageCallback {
- void onLoadImageFinished(Channel channel, int type, Bitmap logo);
- }
-
private Channel() {
// Do nothing.
}
@@ -523,7 +515,6 @@ public final class Channel {
/**
* Prefetches the images for this channel.
*/
- @UiThread
public void prefetchImage(Context context, int type, int maxWidth, int maxHeight) {
String uriString = getImageUriString(type);
if (!TextUtils.isEmpty(uriString)) {
@@ -547,17 +538,9 @@ public final class Channel {
*/
@UiThread
public void loadBitmap(Context context, final int type, int maxWidth, int maxHeight,
- final LoadImageCallback callback) {
+ ImageLoader.ImageLoaderCallback callback) {
String uriString = getImageUriString(type);
- ImageLoader.loadBitmap(context, uriString, maxWidth, maxHeight,
- new ImageLoader.ImageLoaderCallback() {
- @Override
- public void onBitmapLoaded(Bitmap bitmap) {
- if (callback != null) {
- callback.onLoadImageFinished(Channel.this, type, bitmap);
- }
- }
- });
+ ImageLoader.loadBitmap(context, uriString, maxWidth, maxHeight, callback);
}
/**
diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java
index 067f2583..82ac4b5a 100644
--- a/src/com/android/tv/data/ChannelDataManager.java
+++ b/src/com/android/tv/data/ChannelDataManager.java
@@ -28,17 +28,18 @@ import android.media.tv.TvInputManager.TvInputCallback;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.util.MutableInt;
-import com.android.tv.analytics.Tracker;
+import com.android.tv.common.CollectionUtils;
+import com.android.tv.common.SharedPreferencesUtils;
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.SoftPreconditions;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -49,7 +50,6 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.TimeUnit;
/**
* The class to manage channel data.
@@ -58,13 +58,12 @@ import java.util.concurrent.TimeUnit;
* This class is not thread-safe and under an assumption that its public methods are called in
* only the main thread.
*/
+@MainThread
public class ChannelDataManager {
private static final String TAG = "ChannelDataManager";
private static final boolean DEBUG = false;
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;
@@ -72,9 +71,6 @@ public class ChannelDataManager {
private boolean mDbLoadFinished;
private QueryAllChannelsTask mChannelsUpdateTask;
private final List<Runnable> mPostRunnablesAfterChannelUpdate = new ArrayList<>();
- // TODO: move ChannelDataManager to TvApplication to consistently run mRecurringRunner.
- private RecurringRunner mRecurringRunner;
- private final Tracker mTracker;
private final Set<Listener> mListeners = CollectionUtils.createSmallSet();
private final Map<Long, ChannelWrapper> mChannelWrapperMap = new HashMap<>();
@@ -104,9 +100,7 @@ public class ChannelDataManager {
}
if (channelAdded) {
Collections.sort(mChannels, mChannelComparator);
- for (Listener l : mListeners) {
- l.onChannelListUpdated();
- }
+ notifyChannelListUpdated();
}
}
@@ -129,9 +123,7 @@ public class ChannelDataManager {
}
}
Collections.sort(mChannels, mChannelComparator);
- for (Listener l : mListeners) {
- l.onChannelListUpdated();
- }
+ notifyChannelListUpdated();
for (ChannelWrapper channel : removedChannels) {
channel.notifyChannelRemoved();
}
@@ -139,13 +131,12 @@ public class ChannelDataManager {
}
};
- public ChannelDataManager(Context context, TvInputManagerHelper inputManager,
- Tracker tracker) {
- this(context, inputManager, tracker, context.getContentResolver());
+ public ChannelDataManager(Context context, TvInputManagerHelper inputManager) {
+ this(context, inputManager, context.getContentResolver());
}
@VisibleForTesting
- ChannelDataManager(Context context, TvInputManagerHelper inputManager, Tracker tracker,
+ ChannelDataManager(Context context, TvInputManagerHelper inputManager,
ContentResolver contentResolver) {
mContext = context;
mInputManager = inputManager;
@@ -162,12 +153,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);
+ mBrowsableSharedPreferences = context.getSharedPreferences(
+ SharedPreferencesUtils.SHARED_PREF_BROWSABLE, Context.MODE_PRIVATE);
}
@VisibleForTesting
@@ -186,8 +174,8 @@ public class ChannelDataManager {
// Should be called directly instead of posting MSG_UPDATE_CHANNELS message to the handler.
// If not, other DB tasks can be executed before channel loading.
handleUpdateChannels();
- mContentResolver.registerContentObserver(
- TvContract.Channels.CONTENT_URI, true, mChannelObserver);
+ mContentResolver.registerContentObserver(TvContract.Channels.CONTENT_URI, true,
+ mChannelObserver);
mInputManager.addCallback(mTvInputCallback);
}
@@ -202,7 +190,6 @@ public class ChannelDataManager {
}
mStarted = false;
mDbLoadFinished = false;
- mRecurringRunner.stop();
ChannelLogoFetcher.stopFetchingChannelLogos();
mInputManager.removeCallback(mTvInputCallback);
@@ -223,14 +210,22 @@ public class ChannelDataManager {
* Adds a {@link Listener}.
*/
public void addListener(Listener listener) {
- mListeners.add(listener);
+ if (DEBUG) Log.d(TAG, "addListener " + listener);
+ SoftPreconditions.checkNotNull(listener);
+ if (listener != null) {
+ mListeners.add(listener);
+ }
}
/**
* Removes a {@link Listener}.
*/
public void removeListener(Listener listener) {
- mListeners.remove(listener);
+ if (DEBUG) Log.d(TAG, "removeListener " + listener);
+ SoftPreconditions.checkNotNull(listener);
+ if (listener != null) {
+ mListeners.remove(listener);
+ }
}
/**
@@ -365,11 +360,26 @@ public class ChannelDataManager {
}
public void notifyChannelBrowsableChanged() {
- for (Listener l : mListeners) {
+ // Copy the original collection to allow the callee to modify the listeners.
+ for (Listener l : mListeners.toArray(new Listener[mListeners.size()])) {
l.onChannelBrowsableChanged();
}
}
+ private void notifyChannelListUpdated() {
+ // Copy the original collection to allow the callee to modify the listeners.
+ for (Listener l : mListeners.toArray(new Listener[mListeners.size()])) {
+ l.onChannelListUpdated();
+ }
+ }
+
+ private void notifyLoadFinished() {
+ // Copy the original collection to allow the callee to modify the listeners.
+ for (Listener l : mListeners.toArray(new Listener[mListeners.size()])) {
+ l.onLoadFinished();
+ }
+ }
+
/**
* Updates channels from DB. Once the update is done, {@code postRunnable} will
* be called.
@@ -652,14 +662,9 @@ public class ChannelDataManager {
if (!mDbLoadFinished) {
mDbLoadFinished = true;
- mRecurringRunner.start();
- for (Listener l : mListeners) {
- l.onLoadFinished();
- }
+ notifyLoadFinished();
} else if (channelAdded || channelUpdated || channelRemoved) {
- for (Listener l : mListeners) {
- l.onChannelListUpdated();
- }
+ notifyChannelListUpdated();
}
for (ChannelWrapper channelWrapper : removedChannelWrappers) {
channelWrapper.notifyChannelRemoved();
@@ -713,17 +718,4 @@ public class ChannelDataManager {
}
}
}
-
- private class SendChannelStatusRunnable implements Runnable {
- @Override
- public void run() {
- int browsableChannelCount = 0;
- for (Channel channel : mChannels) {
- if (channel.isBrowsable()) {
- ++browsableChannelCount;
- }
- }
- mTracker.sendChannelCount(browsableChannelCount, mChannels.size());
- }
- }
}
diff --git a/src/com/android/tv/data/GenreItems.java b/src/com/android/tv/data/GenreItems.java
index b12fd1aa..92e38809 100644
--- a/src/com/android/tv/data/GenreItems.java
+++ b/src/com/android/tv/data/GenreItems.java
@@ -16,10 +16,14 @@
package com.android.tv.data;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
import android.content.Context;
import android.media.tv.TvContract.Programs.Genres;
+import android.os.Build;
import com.android.tv.R;
+import com.android.tv.common.CollectionUtils;
public class GenreItems {
/**
@@ -27,7 +31,7 @@ public class GenreItems {
*/
public static final int ID_ALL_CHANNELS = 0;
- private static final String[] CANONICAL_GENRES = {
+ private static final String[] CANONICAL_GENRES_BASE = {
null, // All channels
Genres.FAMILY_KIDS,
Genres.SPORTS,
@@ -39,15 +43,30 @@ public class GenreItems {
Genres.EDUCATION,
Genres.ANIMAL_WILDLIFE,
Genres.NEWS,
- Genres.GAMING,
- Genres.ARTS,
- Genres.ENTERTAINMENT,
- Genres.LIFE_STYLE,
- Genres.MUSIC,
- Genres.PREMIER,
- Genres.TECH_SCIENCE
+ Genres.GAMING
};
+ @SuppressLint("InlinedApi")
+ private static final String[] CANONICAL_GENRES_ADDED_IN_L_MR1 = {
+ Genres.ARTS,
+ Genres.ENTERTAINMENT,
+ Genres.LIFE_STYLE,
+ Genres.MUSIC,
+ Genres.PREMIER,
+ Genres.TECH_SCIENCE
+ };
+
+ private static final String[] CANONICAL_GENRES = createGenres();
+
+ private static String[] createGenres() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
+ return CANONICAL_GENRES_BASE;
+ } else {
+ return CollectionUtils
+ .concatAll(CANONICAL_GENRES_BASE, CANONICAL_GENRES_ADDED_IN_L_MR1);
+ }
+ }
+
private GenreItems() { }
/**
diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/Program.java
index a0a5c090..b9c54aac 100644
--- a/src/com/android/tv/data/Program.java
+++ b/src/com/android/tv/data/Program.java
@@ -18,7 +18,6 @@ package com.android.tv.data;
import android.content.Context;
import android.database.Cursor;
-import android.graphics.Bitmap;
import android.media.tv.TvContentRating;
import android.media.tv.TvContract;
import android.support.annotation.NonNull;
@@ -27,6 +26,8 @@ import android.text.TextUtils;
import android.util.Log;
import com.android.tv.R;
+import com.android.tv.common.BuildConfig;
+import com.android.tv.common.TvContentRatingCache;
import com.android.tv.dvr.provider.DvrContract;
import com.android.tv.util.ImageLoader;
import com.android.tv.util.Utils;
@@ -87,7 +88,8 @@ public final class Program implements Comparable<Program> {
builder.setPosterArtUri(cursor.getString(index++));
builder.setThumbnailUri(cursor.getString(index++));
builder.setCanonicalGenres(cursor.getString(index++));
- builder.setContentRatings(Utils.stringToContentRatings(cursor.getString(index++)));
+ builder.setContentRatings(TvContentRatingCache.getInstance()
+ .getRatings(cursor.getString(index++)));
builder.setStartTimeUtcMillis(cursor.getLong(index++));
builder.setEndTimeUtcMillis(cursor.getLong(index++));
builder.setVideoWidth((int) cursor.getLong(index++));
@@ -128,10 +130,6 @@ public final class Program implements Comparable<Program> {
private boolean mRecordable;
private boolean mRecordingScheduled;
- public interface LoadPosterArtCallback {
- void onLoadPosterArtFinished(Program program, Bitmap posterArt);
- }
-
private Program() {
// Do nothing.
}
@@ -296,7 +294,8 @@ public final class Program implements Comparable<Program> {
.append(", endTimeUtcSec=").append(Utils.toTimeString(mEndTimeUtcMillis))
.append(", videoWidth=").append(mVideoWidth)
.append(", videoHeight=").append(mVideoHeight)
- .append(", contentRatings=").append(Utils.contentRatingsToString(mContentRatings))
+ .append(", contentRatings=")
+ .append(TvContentRatingCache.contentRatingsToString(mContentRatings))
.append(", posterArtUri=").append(mPosterArtUri)
.append(", thumbnailUri=").append(mThumbnailUri)
.append(", canonicalGenres=").append(Arrays.toString(mCanonicalGenreIds));
@@ -446,7 +445,6 @@ public final class Program implements Comparable<Program> {
/**
* Prefetches the program poster art.<p>
*/
- @UiThread
public void prefetchPosterArt(Context context, int posterArtWidth, int posterArtHeight) {
if (mPosterArtUri == null) {
return;
@@ -461,21 +459,25 @@ public final class Program implements Comparable<Program> {
*/
@UiThread
public void loadPosterArt(Context context, int posterArtWidth, int posterArtHeight,
- final LoadPosterArtCallback callback) {
+ ImageLoader.ImageLoaderCallback callback) {
if (mPosterArtUri == null) {
return;
}
- ImageLoader.loadBitmap(context, mPosterArtUri, posterArtWidth, posterArtHeight,
- new ImageLoader.ImageLoaderCallback() {
- @Override
- public void onBitmapLoaded(Bitmap bitmap) {
- if (DEBUG) {
- Log.i(TAG, "Loaded poster art for " + Program.this + ": " + bitmap);
- }
- if (callback != null) {
- callback.onLoadPosterArtFinished(Program.this, bitmap);
- }
- }
- });
+ ImageLoader.loadBitmap(context, mPosterArtUri, posterArtWidth, posterArtHeight, callback);
+ }
+
+ public static boolean isDuplicate(Program p1, Program p2) {
+ if (p1 == null || p2 == null) {
+ return false;
+ }
+ boolean isDuplicate = p1.getChannelId() == p2.getChannelId()
+ && p1.getStartTimeUtcMillis() == p2.getStartTimeUtcMillis()
+ && p1.getEndTimeUtcMillis() == p2.getEndTimeUtcMillis();
+ if (BuildConfig.ENG && isDuplicate) {
+ Log.w(TAG, "Duplicate programs detected! - \"" + p1.getTitle() + "\" and \""
+ + p2.getTitle() + "\"");
+ }
+ return isDuplicate;
}
+
}
diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java
index 3f527433..6c167238 100644
--- a/src/com/android/tv/data/ProgramDataManager.java
+++ b/src/com/android/tv/data/ProgramDataManager.java
@@ -26,16 +26,16 @@ import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.LruCache;
-import com.android.tv.BuildConfig;
+import com.android.tv.common.CollectionUtils;
+import com.android.tv.common.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;
@@ -51,6 +51,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
+@MainThread
public class ProgramDataManager implements MemoryManageable {
private static final String TAG = "ProgramDataManager";
private static final boolean DEBUG = false;
@@ -454,7 +455,7 @@ public class ProgramDataManager implements MemoryManageable {
return null;
}
Program program = Program.fromCursor(c);
- if (isDuplicateProgram(program, lastReadProgram)) {
+ if (Program.isDuplicate(program, lastReadProgram)) {
duplicateCount++;
continue;
} else {
@@ -537,7 +538,7 @@ public class ProgramDataManager implements MemoryManageable {
return programs;
}
Program program = Program.fromCursor(c);
- if (isDuplicateProgram(program, lastReadProgram)) {
+ if (Program.isDuplicate(program, lastReadProgram)) {
duplicateCount++;
continue;
} else {
@@ -712,20 +713,6 @@ public class ProgramDataManager implements MemoryManageable {
.setEndTimeUtcMillis(endTimeMs).build();
}
- private static boolean isDuplicateProgram(Program p1, Program p2) {
- if (p1 == null || p2 == null) {
- return false;
- }
- boolean isDuplicate = p1.getChannelId() == p2.getChannelId()
- && p1.getStartTimeUtcMillis() == p2.getStartTimeUtcMillis()
- && p1.getEndTimeUtcMillis() == p2.getEndTimeUtcMillis();
- if (BuildConfig.ENG && isDuplicate) {
- Log.w(TAG, "Duplicate programs detected! - \"" + p1.getTitle() + "\" and \""
- + p2.getTitle() + "\"");
- }
- return isDuplicate;
- }
-
@Override
public void performTrimMemory(int level) {
mChannelId2ProgramUpdatedListeners.clearEmptyCache();
diff --git a/src/com/android/tv/data/TvInputNewComparator.java b/src/com/android/tv/data/TvInputNewComparator.java
index 11993d00..acc3e38a 100644
--- a/src/com/android/tv/data/TvInputNewComparator.java
+++ b/src/com/android/tv/data/TvInputNewComparator.java
@@ -40,8 +40,18 @@ public class TvInputNewComparator implements Comparator<TvInputInfo> {
boolean lhsIsNewInput = mSetupUtils.isNewInput(lhs.getId());
boolean rhsIsNewInput = mSetupUtils.isNewInput(rhs.getId());
if (lhsIsNewInput != rhsIsNewInput) {
+ // New input first.
return lhsIsNewInput ? -1 : 1;
}
+ if (!lhsIsNewInput) {
+ // Checks only when the inputs are not new.
+ boolean lhsSetupDone = mSetupUtils.isSetupDone(lhs.getId());
+ boolean rhsSetupDone = mSetupUtils.isSetupDone(rhs.getId());
+ if (lhsSetupDone != rhsSetupDone) {
+ // An input which has not been setup comes first.
+ return lhsSetupDone ? 1 : -1;
+ }
+ }
return mInputManager.getDefaultTvInputInfoComparator().compare(lhs, rhs);
}
}
diff --git a/src/com/android/tv/data/WatchedHistoryManager.java b/src/com/android/tv/data/WatchedHistoryManager.java
index 13707ae6..cff8cd5c 100644
--- a/src/com/android/tv/data/WatchedHistoryManager.java
+++ b/src/com/android/tv/data/WatchedHistoryManager.java
@@ -10,6 +10,8 @@ import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
+import com.android.tv.common.SharedPreferencesUtils;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -30,7 +32,6 @@ public class 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);
@@ -108,7 +109,7 @@ public class WatchedHistoryManager {
@Override
protected Void doInBackground(Void... params) {
mSharedPreferences = mContext.getSharedPreferences(
- SHARED_PREF_WATCHED_HISTORY, Context.MODE_PRIVATE);
+ SharedPreferencesUtils.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) {
diff --git a/src/com/android/tv/dialog/FullscreenDialogFragment.java b/src/com/android/tv/dialog/FullscreenDialogFragment.java
index 75539f51..eb84aaf9 100644
--- a/src/com/android/tv/dialog/FullscreenDialogFragment.java
+++ b/src/com/android/tv/dialog/FullscreenDialogFragment.java
@@ -32,25 +32,34 @@ import com.android.tv.R;
*/
public class FullscreenDialogFragment extends SafeDismissDialogFragment {
public static final String DIALOG_TAG = FullscreenDialogFragment.class.getSimpleName();
-
- private final int mViewLayoutResId;
- private final String mTrackerLabel;
- private DialogView mDialogView;
+ public static final String VIEW_LAYOUT_ID = "viewLayoutId";
+ public static final String TRACKER_LABEL = "trackerLabel";
/**
- * Constructor of FullscreenDialogFragment. View class of viewLayoutResId should
+ * Creates a FullscreenDialogFragment. View class of viewLayoutResId should
* implement {@link DialogView}.
*/
- public FullscreenDialogFragment(int viewLayoutResId, String trackerLabel) {
- mViewLayoutResId = viewLayoutResId;
- mTrackerLabel = trackerLabel;
+ public static FullscreenDialogFragment newInstance(int viewLayoutResId, String trackerLabel) {
+ FullscreenDialogFragment f = new FullscreenDialogFragment();
+ Bundle args = new Bundle();
+ args.putInt(VIEW_LAYOUT_ID, viewLayoutResId);
+ args.putString(TRACKER_LABEL, trackerLabel);
+ f.setArguments(args);
+ return f;
}
+ private int mViewLayoutResId;
+ private String mTrackerLabel;
+ private DialogView mDialogView;
+
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
FullscreenDialog dialog =
new FullscreenDialog(getActivity(), R.style.Theme_TV_dialog_Fullscreen);
LayoutInflater inflater = LayoutInflater.from(getActivity());
+ Bundle args = getArguments();
+ mViewLayoutResId = args.getInt(VIEW_LAYOUT_ID);
+ mTrackerLabel = args.getString(TRACKER_LABEL);
View v = inflater.inflate(mViewLayoutResId, null);
dialog.setContentView(v);
mDialogView = (DialogView) v;
diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java
index 0ce5d787..a98b5fa0 100644
--- a/src/com/android/tv/dvr/BaseDvrDataManager.java
+++ b/src/com/android/tv/dvr/BaseDvrDataManager.java
@@ -17,10 +17,11 @@
package com.android.tv.dvr;
import android.content.Context;
+import android.support.annotation.MainThread;
import android.util.Log;
-import com.android.tv.Features;
-import com.android.tv.util.CollectionUtils;
+import com.android.tv.common.CollectionUtils;
+import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.util.SoftPreconditions;
import java.util.Set;
@@ -28,6 +29,7 @@ import java.util.Set;
/**
* Base implementation of @{link DataManagerInternal}.
*/
+@MainThread
public abstract class BaseDvrDataManager implements WritableDvrDataManager {
private final static String TAG = "BaseDvrDataManager";
private final static boolean DEBUG = false;
@@ -35,7 +37,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
private final Set<DvrDataManager.Listener> mListeners = CollectionUtils.createSmallSet();
BaseDvrDataManager (Context context){
- SoftPreconditions.checkFeatureEnabled(context,Features.DVR, TAG);
+ SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG);
}
@Override
diff --git a/src/com/android/tv/dvr/DvrDataManager.java b/src/com/android/tv/dvr/DvrDataManager.java
index bed3ed80..4f8b0525 100644
--- a/src/com/android/tv/dvr/DvrDataManager.java
+++ b/src/com/android/tv/dvr/DvrDataManager.java
@@ -16,6 +16,8 @@
package com.android.tv.dvr;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
import android.util.Range;
import java.util.List;
@@ -23,6 +25,7 @@ import java.util.List;
/**
* Read only data manager.
*/
+@MainThread
public interface DvrDataManager {
long NEXT_START_TIME_NOT_FOUND = -1;
@@ -82,6 +85,12 @@ public interface DvrDataManager {
*/
void removeListener(Listener listener);
+ /**
+ * Returns the recording with the given recordingId or null if is not found
+ */
+ @Nullable
+ Recording getRecording(long recordingId);
+
interface Listener {
void onRecordingAdded(Recording recording);
void onRecordingRemoved(Recording recording);
diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java
index d1c590af..647d9bd7 100644
--- a/src/com/android/tv/dvr/DvrDataManagerImpl.java
+++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java
@@ -17,23 +17,32 @@
package com.android.tv.dvr;
import android.content.Context;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
+import android.util.Log;
import android.util.Range;
import com.android.tv.dvr.Recording.RecordingState;
+import com.android.tv.dvr.provider.AsyncDvrDbTask;
import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryTask;
+import com.android.tv.util.SoftPreconditions;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
/**
* DVR Data manager to handle recordings and schedules.
*/
+@MainThread
public class DvrDataManagerImpl extends BaseDvrDataManager {
+ private static final String TAG = "DvrDataManagerImpl";
+
private Context mContext;
private boolean mLoadFinished;
- private final List<Recording> mRecordings = new ArrayList<>();
+ private final HashMap<Long, Recording> mRecordings = new HashMap<>();
private AsyncDvrQueryTask mQueryTask;
public DvrDataManagerImpl(Context context) {
@@ -47,8 +56,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
protected void onPostExecute(List<Recording> result) {
mQueryTask = null;
mLoadFinished = true;
- mRecordings.addAll(result);
- Collections.sort(mRecordings, Recording.START_TIME_COMPARATOR);
+ for (Recording r : result) {
+ mRecordings.put(r.getId(), r);
+ }
}
};
mQueryTask.executeOnDbThread();
@@ -71,7 +81,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (!mLoadFinished) {
return Collections.emptyList();
}
- return Collections.unmodifiableList(mRecordings);
+ ArrayList<Recording> list = new ArrayList<>(mRecordings.size());
+ list.addAll(mRecordings.values());
+ Collections.sort(list, Recording.START_TIME_COMPARATOR);
+ return Collections.unmodifiableList(list);
}
@Override
@@ -91,7 +104,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
private List<Recording> getRecordingsWithState(@RecordingState int state) {
List<Recording> result = new ArrayList<>();
- for (Recording r : mRecordings) {
+ for (Recording r : mRecordings.values()) {
if (r.getState() == state) {
result.add(r);
}
@@ -107,7 +120,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
@Override
public long getNextScheduledStartTimeAfter(long startTime) {
- return getNextStartTimeAfter(mRecordings, startTime);
+ return getNextStartTimeAfter(getRecordings(), startTime);
}
@VisibleForTesting
@@ -129,7 +142,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
@Override
public List<Recording> getRecordingsThatOverlapWith(Range<Long> period) {
List<Recording> result = new ArrayList<>();
- for (Recording r : mRecordings) {
+ for (Recording r : mRecordings.values()) {
if (r.isOverLapping(period)) {
result.add(r);
}
@@ -137,18 +150,80 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
return result;
}
+ @Nullable
+ @Override
+ public Recording getRecording(long recordingId) {
+ if (mLoadFinished) {
+ return mRecordings.get(recordingId);
+ }
+ return null;
+ }
+
@Override
- public void addRecording(Recording recording) { }
+ public void addRecording(final Recording recording) {
+ new AsyncDvrDbTask.AsyncAddRecordingTask(mContext) {
+ @Override
+ protected void onPostExecute(List<Recording> recordings) {
+ super.onPostExecute(recordings);
+ SoftPreconditions.checkArgument(recordings.size() == 1);
+ for (Recording r : recordings) {
+ if (r.getId() != -1) {
+ mRecordings.put(r.getId(), r);
+ notifyRecordingAdded(r);
+ } else {
+ Log.w(TAG, "Error adding " + r);
+ }
+ }
+
+ }
+ }.executeOnDbThread(recording);
+ }
@Override
public void addSeasonRecording(SeasonRecording seasonRecording) { }
@Override
- public void removeRecording(Recording recording) { }
+ public void removeRecording(final Recording recording) {
+ new AsyncDvrDbTask.AsyncDeleteRecordingTask(mContext) {
+ @Override
+ protected void onPostExecute(List<Integer> counts) {
+ super.onPostExecute(counts);
+ SoftPreconditions.checkArgument(counts.size() == 1);
+ for (Integer c : counts) {
+ if (c == 1) {
+ mRecordings.remove(recording.getId());
+ //TODO change to notifyRecordingUpdated
+ notifyRecordingRemoved(recording);
+ } else {
+ Log.w(TAG, "Error removing " + recording);
+ }
+ }
+
+ }
+ }.executeOnDbThread(recording);
+ }
@Override
public void removeSeasonSchedule(SeasonRecording seasonSchedule) { }
@Override
- public void updateRecording(Recording r) { }
+ public void updateRecording(final Recording recording) {
+ new AsyncDvrDbTask.AsyncUpdateRecordingTask(mContext) {
+ @Override
+ protected void onPostExecute(List<Integer> counts) {
+ super.onPostExecute(counts);
+ SoftPreconditions.checkArgument(counts.size() == 1);
+ for (Integer c : counts) {
+ if (c == 1) {
+ mRecordings.put(recording.getId(), recording);
+ //TODO change to notifyRecordingUpdated
+ notifyRecordingStatusChanged(recording);
+ } else {
+ Log.w(TAG, "Error updating " + recording);
+ }
+ }
+
+ }
+ }.executeOnDbThread(recording);
+ }
}
diff --git a/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java b/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java
index 5dbdaac3..8a19cb29 100644
--- a/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java
+++ b/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java
@@ -17,25 +17,33 @@
package com.android.tv.dvr;
import android.content.Context;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.Range;
+import com.android.tv.util.SoftPreconditions;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
/**
* A DVR Data manager that stores values in memory suitable for testing.
*/
@VisibleForTesting // TODO(DVR): move to testing dir.
+@MainThread
public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager {
+ private final static String TAG = "DvrDataManagerInMemory";
+ private final AtomicLong mNextId = new AtomicLong(1);
private final Map<Long, Recording> mRecordings = new HashMap<>();
private List<SeasonRecording> mSeasonSchedule = new ArrayList<>();
- DvrDataManagerInMemoryImpl(Context context) {
+ public DvrDataManagerInMemoryImpl(Context context) {
super(context);
}
@@ -51,19 +59,17 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager {
@Override
public List<Recording> getFinishedRecordings() {
- //TODO filter
- return new ArrayList(mRecordings.values());
+ return getRecordingsWithState(Recording.STATE_RECORDING_FINISHED);
}
@Override
public List<Recording> getStartedRecordings() {
- return null;
+ return getRecordingsWithState(Recording.STATE_RECORDING_IN_PROGRESS);
}
@Override
public List<Recording> getScheduledRecordings() {
- //TODO filter
- return new ArrayList(mRecordings.values());
+ return getRecordingsWithState(Recording.STATE_RECORDING_NOT_STARTED);
}
@Override
@@ -101,8 +107,16 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager {
*/
@Override
public void addRecording(Recording recording) {
+ addRecordingInternal(recording);
+ }
+
+ public Recording addRecordingInternal(Recording recording) {
+ SoftPreconditions.checkState(recording.getId() == Recording.ID_NOT_SET, TAG,
+ "expected id of " + Recording.ID_NOT_SET + " but was " + recording);
+ recording = Recording.buildFrom(recording).setId(mNextId.incrementAndGet()).build();
mRecordings.put(recording.getId(), recording);
notifyRecordingAdded(recording);
+ return recording;
}
@Override
@@ -133,7 +147,19 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager {
}
@Nullable
+ @Override
public Recording getRecording(long id) {
return mRecordings.get(id);
}
+
+ @NonNull
+ private List<Recording> getRecordingsWithState(int state) {
+ ArrayList<Recording> result = new ArrayList<>();
+ for (Recording r : mRecordings.values()) {
+ if(r.getState() == state){
+ result.add(r);
+ }
+ }
+ return result;
+ }
}
diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java
index 35b367ba..c62c564b 100644
--- a/src/com/android/tv/dvr/DvrManager.java
+++ b/src/com/android/tv/dvr/DvrManager.java
@@ -17,43 +17,58 @@
package com.android.tv.dvr;
import android.content.Context;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
import android.util.Log;
+import android.util.Range;
import com.android.tv.ApplicationSingletons;
-import com.android.tv.Features;
import com.android.tv.TvApplication;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.recording.RecordingCapability;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.Program;
import com.android.tv.util.SoftPreconditions;
import com.android.tv.util.Utils;
+import java.util.Collections;
import java.util.List;
/**
* DVR manager class to add and remove recordings. UI can modify recording list through this class,
* instead of modifying them directly through {@link DvrDataManager}.
*/
+@MainThread
public class DvrManager {
private final static String TAG = "DvrManager";
private final WritableDvrDataManager mDataManager;
private final ChannelDataManager mChannelDataManager;
+ private final DvrSessionManager mDvrSessionManager;
public DvrManager(Context context) {
- SoftPreconditions.checkFeatureEnabled(context, Features.DVR, TAG);
+ SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG);
ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager();
mChannelDataManager = appSingletons.getChannelDataManager();
+ mDvrSessionManager = appSingletons.getDvrSessionManger();
}
/**
- * Adds a recording schedule for {@code program}.
+ * Schedules a recording for {@code program} instead of the list of recording that conflict.
+ * @param program the program to record
+ * @param recordingsToOverride the possible empty list of recordings that will not be recorded
*/
- public void addSchedule(Program program) {
- Log.i(TAG, "Adding scheduled recording of " + program);
- //TODO: handle error cases
+ public void addSchedule(Program program, List<Recording> recordingsToOverride) {
+ Log.i(TAG,
+ "Adding scheduled recording of " + program + " instead of " + recordingsToOverride);
+ Collections.sort(recordingsToOverride, Recording.PRIORITY_COMPARATOR);
Channel c = mChannelDataManager.getChannel(program.getChannelId());
- Recording r = Recording.builder(c, program).build();
+ long priority = recordingsToOverride.isEmpty() ? Long.MAX_VALUE
+ : recordingsToOverride.get(0).getPriority() - 1;
+ Recording r = Recording.builder(c, program)
+ .setPriority(priority)
+ .build();
mDataManager.addRecording(r);
}
@@ -81,16 +96,40 @@ public class DvrManager {
*/
public void removeRecording(Recording recording) {
Log.i(TAG, "Removing " + recording);
+ // TODO(DVR): ask the TIS to delete the recording and respond to the result.
mDataManager.removeRecording(recording);
}
/**
- * Checks whether {@code program} can be recorded without any conflict. If there is any
- * conflict, {@code outConflictRecordings} will be filled.
+ * Returns priority ordered list of all scheduled recording that will not be recorded if
+ * this program is.
+ *
+ * <p>Any empty list means there is no conflicts. If there is conflict the program must be
+ * scheduled to record with a Priority lower than the first Recording in the list returned.
*/
- public boolean canAddSchedule(Program program, List<Recording> outConflictRecordings) {
- // TODO: implement
- return true;
+ public List<Recording> getScheduledRecordingsThatConflict(Program program) {
+ //TODO(DVR): move to scheduler.
+ //TODO(DVR): deal with more than one DvrInputService
+ List<Recording> overLap = mDataManager.getRecordingsThatOverlapWith(getPeriod(program));
+ if (!overLap.isEmpty()) {
+ // TODO(DVR): ignore shows that already won't record.
+ Channel channel = mChannelDataManager.getChannel(program.getChannelId());
+ if (channel != null) {
+ RecordingCapability recordingCapability = mDvrSessionManager
+ .getRecordingCapability(channel.getInputId());
+ int remove = Math.max(0, recordingCapability.maxConcurrentTunedSessions - 1);
+ if (remove >= overLap.size()) {
+ return Collections.EMPTY_LIST;
+ }
+ overLap = overLap.subList(remove, overLap.size() - 1);
+ }
+ }
+ return overLap;
+ }
+
+ @NonNull
+ private static Range getPeriod(Program program) {
+ return new Range(program.getStartTimeUtcMillis(), program.getEndTimeUtcMillis());
}
/**
@@ -101,4 +140,13 @@ public class DvrManager {
// TODO: implement
return true;
}
+
+ /**
+ * Returns true is the inputId supports recording.
+ */
+ public boolean canRecord(String inputId) {
+ RecordingCapability recordingCapability = mDvrSessionManager
+ .getRecordingCapability(inputId);
+ return recordingCapability != null && recordingCapability.maxConcurrentTunedSessions > 0;
+ }
}
diff --git a/src/com/android/tv/dvr/DvrPlayActivity.java b/src/com/android/tv/dvr/DvrPlayActivity.java
new file mode 100644
index 00000000..872e05bd
--- /dev/null
+++ b/src/com/android/tv/dvr/DvrPlayActivity.java
@@ -0,0 +1,47 @@
+/*
+ * 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.dvr;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.TextView;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+
+/**
+ * Simple Activity to play a {@link Recording}.
+ */
+public class DvrPlayActivity extends Activity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.dvr_play);
+
+ DvrDataManager dvrDataManager = TvApplication.getSingletons(this).getDvrDataManager();
+ // TODO(DVR) handle errors.
+ long recordingId = getIntent().getLongExtra(Recording.RECORDING_ID_EXTRA, 0);
+ Recording recording = dvrDataManager.getRecording(recordingId);
+ TextView textView = (TextView) findViewById(R.id.placeHolderText);
+ if (recording != null) {
+ textView.setText(recording.toString());
+ } else {
+ textView.setText(R.string.ut_result_not_found_title); // TODO(DVR) update error text
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/DvrRecordingService.java b/src/com/android/tv/dvr/DvrRecordingService.java
index d7a044ab..d0e86d50 100644
--- a/src/com/android/tv/dvr/DvrRecordingService.java
+++ b/src/com/android/tv/dvr/DvrRecordingService.java
@@ -21,14 +21,15 @@ import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
+import android.os.HandlerThread;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.tv.ApplicationSingletons;
-import com.android.tv.Features;
import com.android.tv.TvApplication;
+import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.util.Clock;
import com.android.tv.util.SoftPreconditions;
@@ -49,12 +50,13 @@ import com.android.tv.util.SoftPreconditions;
public class DvrRecordingService extends Service {
private static final String TAG = "DvrRecordingService";
private static final boolean DEBUG = false;
+ public static final String HANDLER_THREAD_NAME = "DvrRecordingService-handler";
+
public static void startService(Context context) {
Intent dvrSchedulerIntent = new Intent(context, DvrRecordingService.class);
context.startService(dvrSchedulerIntent);
}
- private DvrSessionManager mSessionManager;
private WritableDvrDataManager mDataManager;
/**
@@ -71,20 +73,24 @@ public class DvrRecordingService extends Service {
private final IBinder mBinder = new SchedulerBinder();
private Scheduler mScheduler;
+ private HandlerThread mHandlerThread;
@Override
public void onCreate() {
if (DEBUG) Log.d(TAG, "onCreate");
super.onCreate();
- SoftPreconditions.checkFeatureEnabled(this, Features.DVR, TAG);
+ SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG);
ApplicationSingletons singletons = TvApplication.getSingletons(this);
mDataManager = (WritableDvrDataManager) singletons.getDvrDataManager();
- mSessionManager = singletons.getDvrSessionManger();
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
// mScheduler may have been set for testing.
if (mScheduler == null) {
- mScheduler = new Scheduler(mSessionManager, mDataManager, this, Clock.SYSTEM,
+ DvrSessionManager sessionManager = singletons.getDvrSessionManger();
+ mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME);
+ mHandlerThread.start();
+ mScheduler = new Scheduler(mHandlerThread.getLooper(), sessionManager, mDataManager,
+ this, Clock.SYSTEM,
alarmManager);
}
mDataManager.addListener(mScheduler);
@@ -102,6 +108,10 @@ public class DvrRecordingService extends Service {
if (DEBUG) Log.d(TAG, "onDestroy");
mDataManager.removeListener(mScheduler);
mScheduler = null;
+ if (mHandlerThread != null) {
+ mHandlerThread.quit();
+ mHandlerThread = null;
+ }
super.onDestroy();
}
diff --git a/src/com/android/tv/dvr/DvrSessionManager.java b/src/com/android/tv/dvr/DvrSessionManager.java
index 815dfeb1..553001e2 100644
--- a/src/com/android/tv/dvr/DvrSessionManager.java
+++ b/src/com/android/tv/dvr/DvrSessionManager.java
@@ -16,12 +16,18 @@
package com.android.tv.dvr;
+import android.content.ComponentName;
import android.content.Context;
+import android.media.tv.TvContract;
+import android.support.annotation.Nullable;
+import android.support.v4.util.ArrayMap;
-import com.android.tv.Features;
-import com.android.tv.common.dvr.DvrSessionClient;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.recording.RecordingCapability;
+import com.android.tv.common.recording.TvRecording;
import com.android.tv.data.Channel;
import com.android.tv.util.SoftPreconditions;
+import com.android.usbtuner.tvinput.UsbTunerTvInputService;
/**
* Manages Dvr Sessions.
@@ -33,19 +39,55 @@ import com.android.tv.util.SoftPreconditions;
*/
public class DvrSessionManager {
private final static String TAG = "DvrSessionManager";
+ private final Context mContext;
+ private TvRecording.TvRecordingClient mRecordingClient;
+ private ArrayMap<String, RecordingCapability> mCapabilityMap = new ArrayMap<>();
public DvrSessionManager(Context context) {
- SoftPreconditions.checkFeatureEnabled(context, Features.DVR, TAG);
+ SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG);
+ mContext = context.getApplicationContext();
+ // TODO(DVR): get a session to all clients, for now just get USB a TestInput
+ final String inputId = TvContract
+ .buildInputId(new ComponentName(context, UsbTunerTvInputService.class));
+ mRecordingClient = acquireDvrSession(inputId, null);
+ mRecordingClient.connect(inputId, new TvRecording.ClientCallback() {
+ @Override
+ public void onCapabilityReceived(RecordingCapability capability) {
+ mCapabilityMap.put(inputId, capability);
+ mRecordingClient.release();
+ mRecordingClient = null;
+ }
+ });
+ if (CommonFeatures.DVR.isEnabled(context)) { // STOPSHIP(DVR)
+ String testInputId = "com.android.tv.testinput/.TestTvInputService";
+ mCapabilityMap.put(testInputId,
+ RecordingCapability.builder()
+ .setInputId(testInputId)
+ .setMaxConcurrentPlayingSessions(2)
+ .setMaxConcurrentTunedSessions(2)
+ .setMaxConcurrentSessionsOfAllTypes(3)
+ .build());
+
+ }
}
- public DvrSessionClient acquireDvrSession(String inputId, Channel channel) {
- return null;
+ public TvRecording.TvRecordingClient acquireDvrSession(String inputId, Channel channel) {
+ // TODO(DVR): use input and channel or change API
+ TvRecording.TvRecordingClient sessionClient = new TvRecording.TvRecordingClient(mContext);
+ return sessionClient;
}
public boolean canAcquireDvrSession(String inputId, Channel channel) {
- return false;
+ // TODO(DVR): implement
+ return true;
+ }
+
+ public void releaseDvrSession(TvRecording.TvRecordingClient session) {
+ session.release();
}
- public void releaseDvrSession(DvrSessionClient session) {
+ @Nullable
+ public RecordingCapability getRecordingCapability(String inputId) {
+ return mCapabilityMap.get(inputId);
}
}
diff --git a/src/com/android/tv/dvr/Recording.java b/src/com/android/tv/dvr/Recording.java
index 9695d268..9ecda4da 100644
--- a/src/com/android/tv/dvr/Recording.java
+++ b/src/com/android/tv/dvr/Recording.java
@@ -44,6 +44,11 @@ import java.util.List;
public final class Recording {
private static final String TAG = "Recording";
+ public static final String RECORDING_ID_EXTRA = "extra.dvr.recording.id";
+ public static final String PARAM_INPUT_ID = "input_id";
+
+ public static final long ID_NOT_SET = -1;
+
public static final Comparator<Recording> START_TIME_COMPARATOR = new Comparator<Recording>() {
@Override
public int compare(Recording lhs, Recording rhs) {
@@ -51,6 +56,29 @@ public final class Recording {
}
};
+ public static final Comparator<Recording> PRIORITY_COMPARATOR = new Comparator<Recording>() {
+ @Override
+ public int compare(Recording lhs, Recording rhs) {
+ int value = Long.compare(lhs.mPriority, rhs.mPriority);
+ if (value == 0) {
+ value = Long.compare(lhs.mId, rhs.mId);
+ }
+ return value;
+ }
+ };
+
+ public static final Comparator<Recording> START_TIME_THEN_PRIORITY_COMPARATOR
+ = new Comparator<Recording>() {
+ @Override
+ public int compare(Recording lhs, Recording rhs) {
+ int value = START_TIME_COMPARATOR.compare(lhs, rhs);
+ if (value == 0) {
+ value = PRIORITY_COMPARATOR.compare(lhs, rhs);
+ }
+ return value;
+ }
+ };
+
public static Builder builder(Channel c, Program p) {
return new Builder()
.setChannel(c)
@@ -64,11 +92,13 @@ public final class Recording {
return new Builder()
.setChannel(c)
.setStartTime(startTime)
- .setEndTime(endTime);
+ .setEndTime(endTime)
+ .setType(TYPE_TIMED);
}
public static final class Builder {
- private long mId;
+ private long mId = ID_NOT_SET;
+ private long mPriority = Long.MAX_VALUE;
private Uri mUri;
private Channel mChannel;
private List<Program> mPrograms;
@@ -86,7 +116,12 @@ public final class Recording {
return this;
}
- public Builder setUri(Uri uri) {
+ public Builder setPriority(long priority) {
+ mPriority = priority;
+ return this;
+ }
+
+ private Builder setUri(Uri uri) {
mUri = uri;
return this;
}
@@ -132,7 +167,8 @@ public final class Recording {
}
public Recording build() {
- return new Recording(mId, mUri, mChannel, mPrograms, mType, mStartTime, mEndTime, mSize,
+ return new Recording(mId, mPriority, mUri, mChannel, mPrograms, mType, mStartTime,
+ mEndTime, mSize,
mState, mParentSeasonRecording);
}
}
@@ -150,6 +186,7 @@ public final class Recording {
.setSize(orig.mMediaSize)
.setStartTime(orig.mStartTimeMs)
.setState(orig.mState)
+ .setType(orig.mType)
.setUri(orig.mUri);
}
@@ -169,11 +206,11 @@ public final class Recording {
/**
* Record with given time range.
*/
- private static final int TYPE_TIMED = 1;
+ static final int TYPE_TIMED = 1;
/**
* Record with a given program.
*/
- private static final int TYPE_PROGRAM = 2;
+ static final int TYPE_PROGRAM = 2;
@RecordingType private final int mType;
@@ -183,6 +220,7 @@ public final class Recording {
public static final String[] PROJECTION = {
// Columns must match what is read in Recording.fromCursor()
DvrContract.Recordings._ID,
+ DvrContract.Recordings.COLUMN_PRIORITY,
DvrContract.Recordings.COLUMN_TYPE,
DvrContract.Recordings.COLUMN_URI,
DvrContract.Recordings.COLUMN_CHANNEL_ID,
@@ -198,6 +236,14 @@ public final class Recording {
private final long mId;
/**
+ * The priority of this recording.
+ *
+ * <p> The lowest number is recorded first. If there is a tie in priority then the lower id
+ * wins.
+ */
+ private final long mPriority;
+
+ /**
* The {@link Uri} is used as its identifier with the TIS.
* Note: If the state is STATE_RECORDING_NOT_STARTED, this might be {@code null}.
*/
@@ -224,11 +270,19 @@ public final class Recording {
private final SeasonRecording mParentSeasonRecording;
-
- private Recording(long id, Uri uri, Channel channel, List<Program> programs,
+ private Recording(long id, long priority, Uri uri, Channel channel, List<Program> programs,
@RecordingType int type, long startTime, long endTime, long size,
@RecordingState int state, SeasonRecording parentSeasonRecording) {
mId = id;
+ mPriority = priority;
+ if (uri == null && id >= 0 && channel != null) {
+ uri = new Uri.Builder()
+ .scheme("record")
+ .authority("com.android.tv")
+ .appendPath(Long.toString(mId))
+ .appendQueryParameter(PARAM_INPUT_ID, channel.getInputId())
+ .build();
+ }
mUri = uri;
mChannel = channel;
mPrograms = programs == null ? Collections.EMPTY_LIST : new ArrayList<>(programs);
@@ -317,6 +371,10 @@ public final class Recording {
return mId;
}
+ public long getPriority() {
+ return mPriority;
+ }
+
/**
* Creates {@link Recording} object from the given {@link Cursor}.
*/
@@ -324,6 +382,7 @@ public final class Recording {
Builder builder = new Builder();
int index = -1;
builder.setId(c.getLong(++index));
+ builder.setPriority(c.getLong(++index));
builder.setType(recordingType(c.getString(++index)));
String uri = c.getString(++index);
if (uri != null) {
@@ -405,6 +464,7 @@ public final class Recording {
+ "(startTime=" + Utils.toIsoDateTimeString(mStartTimeMs)
+ ",endTime=" + Utils.toIsoDateTimeString(mEndTimeMs)
+ ",state=" + mState
+ + ",priority=" + mPriority
+ ")";
}
}
diff --git a/src/com/android/tv/dvr/RecordingTask.java b/src/com/android/tv/dvr/RecordingTask.java
index e4da5f19..3bed5e77 100644
--- a/src/com/android/tv/dvr/RecordingTask.java
+++ b/src/com/android/tv/dvr/RecordingTask.java
@@ -17,30 +17,65 @@
package com.android.tv.dvr;
import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
import android.util.Log;
-import com.android.tv.common.dvr.DvrSessionClient;
+import com.android.tv.common.recording.TvRecording;
import com.android.tv.data.Channel;
import com.android.tv.util.Clock;
+import com.android.tv.util.SoftPreconditions;
+import com.android.tv.util.Utils;
import java.util.concurrent.TimeUnit;
/**
- * A runnable that actually starts on stop a recording at the right time.
+ * A Handler that actually starts and stop a recording at the right time.
+ *
+ * <p>This is run on the looper of thread named {@value DvrRecordingService#HANDLER_THREAD_NAME}.
+ * There is only one looper so messages must be handled quickly or start a separate thread.
*/
-class RecordingTask extends DvrSessionClient.Callback implements Runnable {
+@WorkerThread
+class RecordingTask extends TvRecording.ClientCallback implements Handler.Callback {
private static final String TAG = "RecordingTask";
- private static final boolean DEBUG = false;
+ private static final boolean DEBUG = true; //STOPSHIP(DVR)
+
+ @VisibleForTesting
+ static final int MESSAGE_INIT = 1;
+ @VisibleForTesting
+ static final int MESSAGE_START_RECORDING = 2;
+ @VisibleForTesting
+ static final int MESSAGE_STOP_RECORDING = 3;
@VisibleForTesting
static long MS_BEFORE_START = TimeUnit.SECONDS.toMillis(5);
@VisibleForTesting
static long MS_AFTER_END = TimeUnit.SECONDS.toMillis(5);
+
+ //STOPSHIP(DVR) don't use enums.
+ @VisibleForTesting
+ enum State {
+ NOT_STARTED,
+ SESSION_ACQUIRED,
+ CONNECTION_PENDING,
+ CONNECTED,
+ RECORDING_START_REQUESTED,
+ RECORDING_STARTED,
+ ERROR,
+ RELEASED,
+ }
private final DvrSessionManager mSessionManager;
+
private final WritableDvrDataManager mDataManager;
- private final Clock mClock;
+ private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+ private TvRecording.TvRecordingClient mSession;
+ private Handler mHandler;
private Recording mRecording;
+ private State mState = State.NOT_STARTED;
+ private final Clock mClock;
RecordingTask(Recording recording, DvrSessionManager sessionManager,
WritableDvrDataManager dataManager, Clock clock) {
@@ -48,68 +83,186 @@ class RecordingTask extends DvrSessionClient.Callback implements Runnable {
mSessionManager = sessionManager;
mDataManager = dataManager;
mClock = clock;
+
if (DEBUG) Log.d(TAG, "created recording task " + mRecording);
}
- @Override
- public void run() {
- if (DEBUG) Log.d(TAG, "running recording task " + mRecording);
+ public void setHandler(Handler handler) {
+ mHandler = handler;
+ }
- //TODO check recording preconditions
- Channel channel = mRecording.getChannel();
- String inputId = channel.getInputId();
- DvrSessionClient session;
- if (mSessionManager.canAcquireDvrSession(inputId, channel)) {
- session = mSessionManager.acquireDvrSession(inputId, channel);
- } else {
- Log.w(TAG, "Unable to acquire a session for " + mRecording);
- updateRecordingState(Recording.STATE_RECORDING_FAILED);
- return;
- }
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (DEBUG) Log.d(TAG, "handleMessage " + msg);
+ SoftPreconditions
+ .checkState(msg.what == Scheduler.HandlerWrapper.MESSAGE_REMOVE || mHandler != null,
+ TAG, "Null handler trying to handle " + msg);
try {
- session.connect(inputId, this);
-
- // TODO: use handler instead of sleep to respond to events and interrupts
- mClock.sleep(Math.max(0,
- (mRecording.getStartTimeMs() - mClock.currentTimeMillis()) - MS_BEFORE_START));
- if (DEBUG) Log.d(TAG, "Start recording " + mRecording);
-
- session.startRecord(channel.getUri(), getIdAsMediaUri(mRecording));
-
- mClock.sleep(Math.max(0,
- (mRecording.getEndTimeMs() - mClock.currentTimeMillis()) + MS_AFTER_END));
- session.stopRecord();
- if (DEBUG) Log.d(TAG, "Finished recording " + mRecording);
- } finally {
- //TODO Don't release until after onRecordStopped etc.
- mSessionManager.releaseDvrSession(session);
+ switch (msg.what) {
+ case MESSAGE_INIT:
+ handleInit();
+ break;
+ case MESSAGE_START_RECORDING:
+ handleStartRecording();
+ break;
+ case MESSAGE_STOP_RECORDING:
+ handleStopRecording();
+ break;
+ case Scheduler.HandlerWrapper.MESSAGE_REMOVE:
+ // Clear the handler
+ mHandler = null;
+ release();
+ return false;
+ default:
+ SoftPreconditions.checkArgument(false, TAG, "unexpected message type " + msg);
+ }
+ return true;
+ } catch (Exception e) {
+ Log.w(TAG, "Error processing message " + msg + " for " + mRecording, e);
+ failAndQuit();
}
+ return false;
+ }
+
+ @Override
+ public void onConnected() {
+ if (DEBUG) Log.d(TAG, "onConnected");
+ super.onConnected();
+ mState = State.CONNECTED;
+ }
+
+ @Override
+ public void onDisconnected() {
+ if (DEBUG) Log.d(TAG, "onDisconnected");
+ super.onDisconnected();
+ //Do nothing
+ }
+
+ @Override
+ public void onRecordDeleted(Uri mediaUri) {
+ if (DEBUG) Log.d(TAG, "onRecordDeleted " + mediaUri);
+ super.onRecordDeleted(mediaUri);
+ SoftPreconditions.checkState(false, TAG, "unexpected onRecordDeleted");
+
+ }
+
+ @Override
+ public void onRecordDeleteFailed(Uri mediaUri, int reason) {
+ if (DEBUG) Log.d(TAG, "onRecordDeleteFailed " + mediaUri + ", " + reason);
+ super.onRecordDeleteFailed(mediaUri, reason);
+ SoftPreconditions.checkState(false, TAG, "unexpected onRecordDeleteFailed");
}
@Override
public void onRecordStarted(Uri mediaUri) {
if (DEBUG) Log.d(TAG, "onRecordStarted " + mediaUri);
super.onRecordStarted(mediaUri);
+ mState = State.RECORDING_STARTED;
updateRecording(Recording.buildFrom(mRecording)
.setState(Recording.STATE_RECORDING_IN_PROGRESS)
- .setUri(mediaUri)
.build());
}
@Override
- public void onRecordStopped(Uri mediaUri, @DvrSessionClient.RecordStopReason int reason) {
+ public void onRecordStopped(Uri mediaUri, @TvRecording.RecordStopReason int reason) {
if (DEBUG) Log.d(TAG, "onRecordStopped " + mediaUri + " reason " + reason);
super.onRecordStopped(mediaUri, reason);
-
- //TODO need a success reason.
- switch (reason){
+ // TODO(dvr) handle success
+ switch (reason) {
default:
updateRecording(Recording.buildFrom(mRecording)
.setState(Recording.STATE_RECORDING_FAILED)
.build());
- }
+ }
+ release();
+ sendRemove();
}
+ private void handleInit() {
+ //TODO check recording preconditions
+ Channel channel = mRecording.getChannel();
+ if (channel == null) {
+ Log.w(TAG, "Null channel for " + mRecording);
+ failAndQuit();
+ return;
+ }
+
+ String inputId = channel.getInputId();
+ if (mSessionManager.canAcquireDvrSession(inputId, channel)) {
+ mSession = mSessionManager.acquireDvrSession(inputId, channel);
+ mState = State.SESSION_ACQUIRED;
+ } else {
+ Log.w(TAG, "Unable to acquire a session for " + mRecording);
+ failAndQuit();
+ return;
+ }
+
+ mSession.connect(inputId, this);
+ mState = State.CONNECTION_PENDING;
+
+ if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_START_RECORDING,
+ mRecording.getStartTimeMs() - MS_BEFORE_START)) {
+ mState = State.ERROR;
+ return;
+ }
+ }
+
+ private void failAndQuit() {
+ updateRecordingState(Recording.STATE_RECORDING_FAILED);
+ mState = State.ERROR;
+ sendRemove();
+ }
+
+ private void sendRemove() {
+ if (mHandler != null) {
+ mHandler.sendEmptyMessage(Scheduler.HandlerWrapper.MESSAGE_REMOVE);
+ }
+ }
+
+ private void handleStartRecording() {
+ if (DEBUG)Log.d(TAG, "handleStartRecording " + mRecording);
+ // TODO(DVR) handle errors
+ Channel channel = mRecording.getChannel();
+ mSession.startRecord(channel.getUri(), getIdAsMediaUri(mRecording));
+ mState= State.RECORDING_START_REQUESTED;
+ if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_STOP_RECORDING,
+ mRecording.getEndTimeMs() + MS_AFTER_END)) {
+ mState = State.ERROR;
+ return;
+ }
+ }
+
+ private void handleStopRecording() {
+ if (DEBUG)Log.d(TAG, "handleStopRecording " + mRecording);
+ mSession.stopRecord();
+ // TODO: once we add an API to notify successful completion of recording,
+ // the following parts need to be moved to the listener implementation.
+ updateRecording(Recording.buildFrom(mRecording)
+ .setState(Recording.STATE_RECORDING_FINISHED).build());
+ sendRemove();
+ }
+
+ @VisibleForTesting
+ State getState() {
+ return mState;
+ }
+
+ private void release() {
+ if (mSession != null) {
+ mSession.release();
+ mSessionManager.releaseDvrSession(mSession);
+ }
+ }
+
+ private boolean sendEmptyMessageAtAbsoluteTime(int what, long when) {
+ long now = mClock.currentTimeMillis();
+ long delay = Math.max(0L, when - now);
+ if (DEBUG) {
+ Log.d(TAG, "Sending message " + what + " with a delay of " + delay / 1000
+ + " seconds to arrive at " + Utils.toIsoDateTimeString(when));
+ }
+ return mHandler.sendEmptyMessageDelayed(what, delay);
+ }
private void updateRecordingState(@Recording.RecordingState int state) {
updateRecording(Recording.buildFrom(mRecording).setState(state).build());
@@ -123,7 +276,12 @@ class RecordingTask extends DvrSessionClient.Callback implements Runnable {
private void updateRecording(Recording updatedRecording) {
if (DEBUG) Log.d(TAG, "updateRecording " + updatedRecording);
mRecording = updatedRecording;
- mDataManager.updateRecording(mRecording);
+ mMainThreadHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mDataManager.updateRecording(mRecording);
+ }
+ });
}
@Override
diff --git a/src/com/android/tv/dvr/Scheduler.java b/src/com/android/tv/dvr/Scheduler.java
index 0787adc3..8070f8a6 100644
--- a/src/com/android/tv/dvr/Scheduler.java
+++ b/src/com/android/tv/dvr/Scheduler.java
@@ -20,18 +20,17 @@ import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.Range;
import com.android.tv.util.Clock;
-import com.android.tv.util.NamedThreadFactory;
import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
/**
@@ -42,44 +41,46 @@ public class Scheduler implements DvrDataManager.Listener {
private static final String TAG = "Scheduler";
private static final boolean DEBUG = false;
+ private final static long SOON_DURATION_IN_MS = TimeUnit.MINUTES.toMillis(5);
+ @VisibleForTesting final static long MS_TO_WAKE_BEFORE_START = TimeUnit.MINUTES.toMillis(1);
+
/**
- * Wraps a RecordingTask removing it from {@link #mPendingRecordings} when it is done.
+ * Wraps a {@link RecordingTask} removing it from {@link #mPendingRecordings} when it is done.
*/
- private final class TaskWrapper extends FutureTask<Void> {
+ public final class HandlerWrapper extends Handler {
+ public static final int MESSAGE_REMOVE = 999;
private final long mId;
- TaskWrapper(Recording recording) {
- super(new RecordingTask(recording, mSessionManager, mDataManager, mClock), null);
+ HandlerWrapper(Looper looper, Recording recording, RecordingTask recordingTask) {
+ super(looper, recordingTask);
mId = recording.getId();
}
@Override
- public void done() {
- if (DEBUG) Log.d(TAG, "done " + mId);
- mPendingRecordings.remove(mId);
- super.done();
+ public void handleMessage(Message msg) {
+ // The RecordingTask gets a chance first.
+ // It must return false to pass this message to here.
+ if (msg.what == MESSAGE_REMOVE) {
+ if (DEBUG) Log.d(TAG, "done " + mId);
+ mPendingRecordings.remove(mId);
+ }
+ removeCallbacksAndMessages(null);
+ super.handleMessage(msg);
}
}
+ private final LongSparseArray<HandlerWrapper> mPendingRecordings = new LongSparseArray<>();
+ private final Looper mLooper;
+ private final DvrSessionManager mSessionManager;
private final WritableDvrDataManager mDataManager;
private final Context mContext;
- private final DvrSessionManager mSessionManager;
- private PendingIntent mAlarmIntent;
-
- private static final NamedThreadFactory sNamedThreadFactory = new NamedThreadFactory(
- "DVR-scheduler");
- @VisibleForTesting final static long MS_TO_WAKE_BEFORE_START = TimeUnit.MINUTES.toMillis(1);
- private final static long SOON_DURATION_IN_MS = TimeUnit.MINUTES.toMillis(5);
-
- private final ExecutorService mExecutorService = Executors
- .newCachedThreadPool(sNamedThreadFactory);
- private final LongSparseArray<TaskWrapper> mPendingRecordings = new LongSparseArray<>();
private final Clock mClock;
private final AlarmManager mAlarmManager;
- public Scheduler(DvrSessionManager sessionManager, WritableDvrDataManager dataManager,
- Context context, Clock clock,
+ public Scheduler(Looper looper, DvrSessionManager sessionManager,
+ WritableDvrDataManager dataManager, Context context, Clock clock,
AlarmManager alarmManager) {
+ mLooper = looper;
mSessionManager = sessionManager;
mDataManager = dataManager;
mContext = context;
@@ -106,7 +107,6 @@ public class Scheduler implements DvrDataManager.Listener {
updateNextAlarm();
}
-
@Override
public void onRecordingAdded(Recording recording) {
if (DEBUG) Log.d(TAG, "added " + recording);
@@ -120,9 +120,9 @@ public class Scheduler implements DvrDataManager.Listener {
@Override
public void onRecordingRemoved(Recording recording) {
long id = recording.getId();
- TaskWrapper task = mPendingRecordings.get(id);
- if (task != null) {
- task.cancel(true);
+ HandlerWrapper wrapper = mPendingRecordings.get(id);
+ if (wrapper != null) {
+ wrapper.removeCallbacksAndMessages(null);
mPendingRecordings.remove(id);
} else {
updateNextAlarm();
@@ -134,12 +134,13 @@ public class Scheduler implements DvrDataManager.Listener {
//TODO(DVR): implement
}
-
private void scheduleRecordingSoon(Recording recording) {
- // TODO(DVR) test match in mPendingRecordings recordings.
- TaskWrapper task = new TaskWrapper(recording);
- mPendingRecordings.put(recording.getId(), task);
- mExecutorService.submit(task);
+ RecordingTask recordingTask = new RecordingTask(recording, mSessionManager, mDataManager,
+ mClock);
+ HandlerWrapper handlerWrapper = new HandlerWrapper(mLooper, recording, recordingTask);
+ recordingTask.setHandler(handlerWrapper);
+ mPendingRecordings.put(recording.getId(), handlerWrapper);
+ handlerWrapper.sendEmptyMessage(RecordingTask.MESSAGE_INIT);
}
private void updateNextAlarm() {
@@ -149,9 +150,9 @@ public class Scheduler implements DvrDataManager.Listener {
long wakeAt = nextStartTime - MS_TO_WAKE_BEFORE_START;
if (DEBUG) Log.d(TAG, "Set alarm to record at " + wakeAt);
Intent intent = new Intent(mContext, DvrStartRecordingReceiver.class);
- mAlarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+ PendingIntent alarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
//This will cancel the previous alarm.
- mAlarmManager.set(AlarmManager.RTC_WAKEUP, wakeAt, mAlarmIntent);
+ mAlarmManager.set(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent);
} else {
if (DEBUG) Log.d(TAG, "No future recording, alarm not set");
}
diff --git a/src/com/android/tv/dvr/WritableDvrDataManager.java b/src/com/android/tv/dvr/WritableDvrDataManager.java
index d1ba702e..87809701 100644
--- a/src/com/android/tv/dvr/WritableDvrDataManager.java
+++ b/src/com/android/tv/dvr/WritableDvrDataManager.java
@@ -16,12 +16,15 @@
package com.android.tv.dvr;
+import android.support.annotation.MainThread;
+
/**
* Full data manager.
*
* <p>The following operations need to be synced with permanent storage. The following commands are
* for internal use only. Do not call them from UI directly.
*/
+@MainThread
interface WritableDvrDataManager extends DvrDataManager {
/**
* Add a new recording.
diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
index 55748937..3fc6e4a9 100644
--- a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
+++ b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
@@ -19,6 +19,7 @@ package com.android.tv.dvr.provider;
import android.content.Context;
import android.database.Cursor;
import android.os.AsyncTask;
+import android.support.annotation.Nullable;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
@@ -69,6 +70,71 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result>
executeOnExecutor(DB_EXECUTOR, params);
}
+ @Override
+ protected final Result doInBackground(Params... params) {
+ initializeDbHelper(mContext);
+ return doInDvrBackground(params);
+ }
+
+ /**
+ * Executes in the background after {@link #initializeDbHelper(Context)}
+ */
+ @Nullable
+ protected abstract Result doInDvrBackground(Params... params);
+
+ /**
+ * Inserts recordings returning the list of recordings with id set.
+ * The id will be -1 if there was an error.
+ */
+ public abstract static class AsyncAddRecordingTask
+ extends AsyncDvrDbTask<Recording, Void, List<Recording>> {
+
+ public AsyncAddRecordingTask(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected final List<Recording> doInDvrBackground(Recording... params) {
+ return sDbHelper.insertRecordings(params);
+ }
+ }
+
+ /**
+ * Update recordings.
+ *
+ * @return list of row update counts. The count will be -1 if there was an error or 0
+ * if no match was found. The count is expected to be exactly 1 for each recording.
+ */
+ public abstract static class AsyncUpdateRecordingTask
+ extends AsyncDvrDbTask<Recording, Void, List<Integer>> {
+ public AsyncUpdateRecordingTask(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected final List<Integer> doInDvrBackground(Recording... params) {
+ return sDbHelper.updateRecordings(params);
+ }
+ }
+
+ /**
+ * Delete recordings.
+ *
+ * @return list of row delete counts. The count will be -1 if there was an error or 0
+ * if no match was found. The count is expected to be exactly 1 for each recording.
+ */
+ public abstract static class AsyncDeleteRecordingTask
+ extends AsyncDvrDbTask<Recording, Void, List<Integer>> {
+ public AsyncDeleteRecordingTask(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected final List<Integer> doInDvrBackground(Recording... params) {
+ return sDbHelper.deleteRecordings(params);
+ }
+ }
+
public abstract static class AsyncDvrQueryTask
extends AsyncDvrDbTask<Void, Void, List<Recording>> {
public AsyncDvrQueryTask(Context context) {
@@ -76,9 +142,8 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result>
}
@Override
- protected List<Recording> doInBackground(Void... params) {
- initializeDbHelper(mContext);
-
+ @Nullable
+ protected final List<Recording> doInDvrBackground(Void... params) {
if (isCancelled()) {
return null;
}
@@ -116,6 +181,7 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result>
List<Long> programList = recordingToProgramMap.get(recordingId);
if (programList == null) {
programList = new ArrayList<>();
+ recordingToProgramMap.put(recordingId, programList);
}
programList.add(c.getLong(1));
}
diff --git a/src/com/android/tv/dvr/provider/DvrContract.java b/src/com/android/tv/dvr/provider/DvrContract.java
index 650de2e4..e6ce4141 100644
--- a/src/com/android/tv/dvr/provider/DvrContract.java
+++ b/src/com/android/tv/dvr/provider/DvrContract.java
@@ -51,6 +51,16 @@ public final class DvrContract {
public static final String STATE_RECORDING_FINISHED = "STATE_RECORDING_FINISHED";
/**
+ * The priority of this recording.
+ *
+ * <p> The lowest number is recorded first. If there is a tie in priority then the lower id
+ * wins. Defaults to {@value Long#MAX_VALUE}
+ *
+ * <p>Type: INTEGER (long)
+ */
+ public static final String COLUMN_PRIORITY = "priority";
+
+ /**
* The type of this recording.
*
* <p>This value should be one of the followings: {@link #TYPE_PROGRAM},
diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
index e9bfc340..2445e935 100644
--- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
+++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
@@ -16,6 +16,7 @@
package com.android.tv.dvr.provider;
+import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
@@ -23,11 +24,16 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.util.Log;
+import com.android.tv.data.Channel;
+import com.android.tv.dvr.Recording;
import com.android.tv.dvr.provider.DvrContract.DvrChannels;
import com.android.tv.dvr.provider.DvrContract.DvrPrograms;
import com.android.tv.dvr.provider.DvrContract.RecordingToPrograms;
import com.android.tv.dvr.provider.DvrContract.Recordings;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* A data class for one recorded contents.
*/
@@ -35,12 +41,13 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = "DvrDatabaseHelper";
private static final boolean DEBUG = true;
- private static final int DATABASE_VERSION = 1;
+ private static final int DATABASE_VERSION = 2;
private static final String DB_NAME = "dvr.db";
private static final String SQL_CREATE_RECORDINGS =
"CREATE TABLE " + Recordings.TABLE_NAME + "("
- + Recordings._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + Recordings._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + Recordings.COLUMN_PRIORITY
+ + " INTEGER DEFAULT " + Long.MAX_VALUE + ","
+ Recordings.COLUMN_TYPE + " TEXT NOT NULL,"
+ Recordings.COLUMN_URI + " TEXT,"
+ Recordings.COLUMN_CHANNEL_ID + " INTEGER NOT NULL,"
@@ -77,6 +84,7 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
+ DvrPrograms.TABLE_NAME;
private static final String SQL_DROP_RECORDING_PROGRAMS = "DROP TABLE IF EXISTS "
+ RecordingToPrograms.TABLE_NAME;
+ public static final String WHERE_RECORDING_ID_EQUALS = Recordings._ID + " = ?";
public DvrDatabaseHelper(Context context) {
super(context.getApplicationContext(), DB_NAME, null, DATABASE_VERSION);
@@ -121,4 +129,85 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
builder.setTables(tableName);
return builder.query(db, projections, null, null, null, null, null);
}
+
+ /**
+ * Inserts recordings.
+ *
+ * @return The list of recordings with id set. The id will be -1 if there was an error.
+ */
+ public List<Recording> insertRecordings(Recording... recordings) {
+ updateChannelsFromRecordings(recordings);
+
+ SQLiteDatabase db = getReadableDatabase();
+ List<Recording> results = new ArrayList<>();
+ for (Recording r : recordings) {
+ ContentValues values = getContentValues(r);
+ long id = db.insert(Recordings.TABLE_NAME, null, values);
+ results.add(Recording.buildFrom(r).setId(id).build());
+ }
+ return results;
+ }
+
+ /**
+ * Update recordings.
+ *
+ * @return The list of row update counts. The count will be -1 if there was an error or 0
+ * if no match was found. The count is expected to be exactly 1 for each recording.
+ */
+ public List<Integer> updateRecordings(Recording[] recordings) {
+ updateChannelsFromRecordings(recordings);
+ SQLiteDatabase db = getWritableDatabase();
+ List<Integer> results = new ArrayList<>();
+ long count = 0;
+ for (Recording r : recordings) {
+ ContentValues values = getContentValues(r);
+ int updated = db.update(Recordings.TABLE_NAME, values, Recordings._ID + " = ?",
+ new String[] {String.valueOf(r.getId())});
+ results.add(updated);
+ }
+ return results;
+ }
+
+ private void updateChannelsFromRecordings(Recording[] recordings) {
+ // TODO(DVR) implement/
+ // TODO(DVR) consider not deleting channels instead of keeping a separate table.
+ }
+
+ private ContentValues getContentValues(Recording r) {
+ ContentValues values = new ContentValues();
+ // TODO(DVR): use DVR channel id instead
+ Channel channel = r.getChannel();
+ if (channel != null) {
+ values.put(Recordings.COLUMN_CHANNEL_ID, channel.getId());
+ }
+ values.put(Recordings.COLUMN_PRIORITY, r.getPriority());
+ values.put(Recordings.COLUMN_START_TIME_UTC_MILLIS, r.getStartTimeMs());
+ values.put(Recordings.COLUMN_END_TIME_UTC_MILLIS, r.getEndTimeMs());
+ values.put(Recordings.COLUMN_STATE, r.getState());
+ values.put(Recordings.COLUMN_MEDIA_SIZE, r.getSize());
+ values.put(Recordings.COLUMN_TYPE, r.getType());
+ if (r.getUri() != null) {
+ values.put(Recordings.COLUMN_URI, r.getUri().toString());
+ }
+ return values;
+ }
+
+ /**
+ * Delete recordings.
+ *
+ * @return The list of row update counts. The count will be -1 if there was an error or 0
+ * if no match was found. The count is expected to be exactly 1 for each recording.
+ */
+ public List<Integer> deleteRecordings(Recording[] recordings) {
+ SQLiteDatabase db = getWritableDatabase();
+ List<Integer> results = new ArrayList<>();
+ long count = 0;
+ for (Recording r : recordings) {
+ ContentValues values = getContentValues(r);
+ int deleted = db.delete(Recordings.TABLE_NAME, WHERE_RECORDING_ID_EQUALS,
+ new String[] {String.valueOf(r.getId())});
+ results.add(deleted);
+ }
+ return results;
+ }
}
diff --git a/src/com/android/tv/util/MemoryManageable.java b/src/com/android/tv/dvr/ui/DvrActivity.java
index c5e5d869..01f3fb9c 100644
--- a/src/com/android/tv/util/MemoryManageable.java
+++ b/src/com/android/tv/dvr/ui/DvrActivity.java
@@ -14,16 +14,20 @@
* limitations under the License
*/
-package com.android.tv.util;
+package com.android.tv.dvr.ui;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import com.android.tv.R;
/**
- * 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}.
+ * {@link android.app.Activity} for DVR UI.
*/
-public interface MemoryManageable {
- /**
- * For more information, see {@link android.content.ComponentCallbacks2#onTrimMemory}.
- */
- void performTrimMemory(int level);
+public class DvrActivity extends Activity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.dvr_main);
+ }
}
diff --git a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java
new file mode 100644
index 00000000..87e47930
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java
@@ -0,0 +1,119 @@
+/*
+ * 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.dvr.ui;
+
+import android.os.Bundle;
+import android.support.annotation.IntDef;
+import android.support.v17.leanback.app.BrowseFragment;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.util.Log;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.Recording;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+/**
+ * {@link BrowseFragment} for DVR functions.
+ */
+public class DvrBrowseFragment extends BrowseFragment {
+ private static final String TAG = "DvrBrowseFragment";
+
+ @IntDef({DVR_CURRENT_RECORDINGS, DVR_SCHEDULED_RECORDINGS, DVR_RECORDED_PROGRAMS, DVR_SETTINGS})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DVR_HEADERS_MODE {}
+ public static final int DVR_CURRENT_RECORDINGS = 0;
+ public static final int DVR_SCHEDULED_RECORDINGS = 1;
+ public static final int DVR_RECORDED_PROGRAMS = 2;
+ public static final int DVR_SETTINGS = 3;
+
+ private static LinkedHashMap<Integer, Integer> sHeaders =
+ new LinkedHashMap<Integer, Integer>() {{
+ put(DVR_CURRENT_RECORDINGS, R.string.dvr_main_current_recordings);
+ put(DVR_SCHEDULED_RECORDINGS, R.string.dvr_main_scheduled_recordings);
+ put(DVR_RECORDED_PROGRAMS, R.string.dvr_main_recorded_programs);
+ put(DVR_SETTINGS, R.string.dvr_main_settings);
+ }};
+
+ private DvrDataManager mDvrDataManager;
+ private ArrayObjectAdapter mRowsAdapter;
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ Log.d(TAG, "onCreate");
+ super.onActivityCreated(savedInstanceState);
+ setupUiElements();
+ setupAdapter();
+ prepareEntranceTransition();
+
+ // TODO: load asynchronously.
+ loadData();
+ }
+
+ private void setupUiElements() {
+ setHeadersState(HEADERS_ENABLED);
+ setHeadersTransitionOnBackEnabled(false);
+ }
+
+ private void setupAdapter() {
+ mDvrDataManager = TvApplication.getSingletons(getContext()).getDvrDataManager();
+ mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
+ setAdapter(mRowsAdapter);
+ }
+
+ private void loadRow(ArrayObjectAdapter gridRowAdapter, List<Recording> recordings) {
+ if (recordings == null || recordings.size() == 0) {
+ gridRowAdapter.add(null);
+ return;
+ }
+ for (Recording r : recordings) {
+ gridRowAdapter.add(r);
+ }
+ }
+
+ private void loadData() {
+ for (@DVR_HEADERS_MODE int i : sHeaders.keySet()) {
+ HeaderItem gridHeader = new HeaderItem(i, getContext().getString(sHeaders.get(i)));
+ GridItemPresenter gridPresenter = new GridItemPresenter(this);
+ ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(gridPresenter);
+ switch (i) {
+ case DVR_CURRENT_RECORDINGS:
+ loadRow(gridRowAdapter, mDvrDataManager.getStartedRecordings());
+ break;
+ case DVR_SCHEDULED_RECORDINGS:
+ loadRow(gridRowAdapter, mDvrDataManager.getScheduledRecordings());
+ break;
+ case DVR_RECORDED_PROGRAMS:
+ loadRow(gridRowAdapter, mDvrDataManager.getFinishedRecordings());
+ break;
+ case DVR_SETTINGS:
+ // TODO: provide setup rows.
+ break;
+ }
+ mRowsAdapter.add(new ListRow(gridHeader, gridRowAdapter));
+ }
+ startEntranceTransition();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/GridItemPresenter.java b/src/com/android/tv/dvr/ui/GridItemPresenter.java
new file mode 100644
index 00000000..099816d4
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/GridItemPresenter.java
@@ -0,0 +1,165 @@
+/*
+ * 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.dvr.ui;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.Color;
+import android.support.v17.leanback.widget.Presenter;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.tv.MainActivity;
+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.dvr.DvrManager;
+import com.android.tv.dvr.Recording;
+import com.android.tv.util.Utils;
+
+import java.util.List;
+
+public class GridItemPresenter extends Presenter {
+ private static final int GRID_ITEM_WIDTH = 200;
+ private static final int GRID_ITEM_HEIGHT = 200;
+
+ private final DvrBrowseFragment mainFragment;
+
+ public GridItemPresenter(DvrBrowseFragment mainFragment) {
+ this.mainFragment = mainFragment;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ TextView view = new TextView(parent.getContext());
+ view.setLayoutParams(new ViewGroup.LayoutParams(GRID_ITEM_WIDTH, GRID_ITEM_HEIGHT));
+ view.setFocusable(true);
+ view.setFocusableInTouchMode(true);
+ view.setBackgroundColor(
+ Utils.getColor(mainFragment.getResources(), R.color.setup_background));
+ view.setTextColor(Color.WHITE);
+ view.setGravity(Gravity.CENTER);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder viewHolder, Object recording) {
+ if (recording == null) {
+ ((TextView) viewHolder.view).setText(viewHolder.view.getContext()
+ .getString(R.string.dvr_msg_no_recording_on_the_row));
+ } else {
+ final Recording r = (Recording) recording;
+ StringBuilder sb = new StringBuilder();
+ List<Program> programs = r.getPrograms();
+ if (programs != null && programs.size() > 0) {
+ sb.append(programs.get(0).getTitle());
+ } else {
+ sb.append(viewHolder.view.getContext()
+ .getString(R.string.dvr_msg_program_title_unknown));
+ }
+ sb.append(" ");
+ Channel channel = r.getChannel();
+ if (channel != null) {
+ sb.append(channel.getDisplayName());
+ } else {
+ sb.append(viewHolder.view.getContext().getString(R.string.dvr_msg_channel_unknown));
+ }
+ sb.append(" ").append(Utils.toIsoDateTimeString(r.getStartTimeMs()));
+ ((TextView) viewHolder.view).setText(sb.toString());
+ final Context context = viewHolder.view.getContext();
+ viewHolder.view.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ switch (r.getState()) {
+ case Recording.STATE_RECORDING_NOT_STARTED: {
+ new AlertDialog.Builder(context)
+ .setNegativeButton(R.string.dvr_detail_cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Toast.makeText(context, "Not implemented yet",
+ Toast.LENGTH_SHORT).show();
+ }
+ })
+ .show();
+ break;
+ }
+ case Recording.STATE_RECORDING_IN_PROGRESS: {
+ new AlertDialog.Builder(context)
+ .setNegativeButton(R.string.dvr_detail_stop_delete,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Toast.makeText(context, "Not implemented yet",
+ Toast.LENGTH_SHORT).show();
+ }
+ })
+ .setPositiveButton(R.string.dvr_detail_stop_keep,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Toast.makeText(context, "Not implemented yet",
+ Toast.LENGTH_SHORT).show();
+ }
+ })
+ .show();
+ break;
+ }
+ case Recording.STATE_RECORDING_FINISHED: {
+ new AlertDialog.Builder(context)
+ .setNegativeButton(R.string.dvr_detail_delete,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ DvrManager dvrManager = TvApplication
+ .getSingletons(mainFragment.getContext())
+ .getDvrManager();
+ // TODO(DVR) handle success/failure.
+ dvrManager.removeRecording(r);
+ }
+ })
+ .setPositiveButton(R.string.dvr_detail_play,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent intent = new Intent(context, MainActivity.class);
+ intent.putExtra(Utils.EXTRA_KEY_RECORDING_URI,
+ r.getUri());
+ context.startActivity(intent);
+ ((Activity) context).finish();
+ }
+ })
+ .show();
+ break;
+ }
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onUnbindViewHolder(ViewHolder viewHolder) {
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java
index d4e4a99d..77a1146b 100644
--- a/src/com/android/tv/guide/ProgramGuide.java
+++ b/src/com/android/tv/guide/ProgramGuide.java
@@ -252,32 +252,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
// It is usually called when Genre is changed.
// Reset selection of ProgramGrid
resetRowSelection();
-
- // Align EPG at vertical center, if EPG table height is less than the screen size.
- Resources res = mActivity.getResources();
- int screenHeight = mContainer.getHeight();
- int startPadding = res
- .getDimensionPixelOffset(R.dimen.program_guide_table_margin_start);
- int topPadding = res
- .getDimensionPixelOffset(R.dimen.program_guide_table_margin_top);
- int bottomPadding = res
- .getDimensionPixelOffset(R.dimen.program_guide_table_margin_bottom);
- int tableHeight =
- res.getDimensionPixelOffset(R.dimen.program_guide_table_header_row_height)
- + mDetailHeight + mRowHeight * mGrid.getAdapter().getItemCount()
- + topPadding + bottomPadding;
- if (tableHeight > screenHeight) {
- // EPG height is longer that the screen height.
- mTable.setPaddingRelative(startPadding, topPadding, 0, 0);
- LayoutParams layoutParams = mTable.getLayoutParams();
- layoutParams.height = LayoutParams.WRAP_CONTENT;
- mTable.setLayoutParams(layoutParams);
- } else {
- mTable.setPaddingRelative(startPadding, topPadding, 0, bottomPadding);
- LayoutParams layoutParams = mTable.getLayoutParams();
- layoutParams.height = tableHeight;
- mTable.setLayoutParams(layoutParams);
- }
+ updateGuidePosition();
}
});
@@ -399,6 +374,34 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
mShowGuidePartial = mSharedPreference.getBoolean(KEY_SHOW_GUIDE_PARTIAL, true);
}
+ private void updateGuidePosition() {
+ // Align EPG at vertical center, if EPG table height is less than the screen size.
+ Resources res = mActivity.getResources();
+ int screenHeight = mContainer.getHeight();
+ if (screenHeight <= 0) {
+ // mContainer is not initialized yet.
+ return;
+ }
+ int startPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_start);
+ int topPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_top);
+ int bottomPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_bottom);
+ int tableHeight = res.getDimensionPixelOffset(R.dimen.program_guide_table_header_row_height)
+ + mDetailHeight + mRowHeight * mGrid.getAdapter().getItemCount() + topPadding
+ + bottomPadding;
+ if (tableHeight > screenHeight) {
+ // EPG height is longer that the screen height.
+ mTable.setPaddingRelative(startPadding, topPadding, 0, 0);
+ LayoutParams layoutParams = mTable.getLayoutParams();
+ layoutParams.height = LayoutParams.WRAP_CONTENT;
+ mTable.setLayoutParams(layoutParams);
+ } else {
+ mTable.setPaddingRelative(startPadding, topPadding, 0, bottomPadding);
+ LayoutParams layoutParams = mTable.getLayoutParams();
+ layoutParams.height = tableHeight;
+ mTable.setLayoutParams(layoutParams);
+ }
+ }
+
@Override
public void onRequestChildFocus(View oldFocus, View newFocus) {
if (oldFocus != null && newFocus != null) {
@@ -510,18 +513,19 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
if (DEBUG) {
mContainer.getViewTreeObserver().addOnDrawListener(
new ViewTreeObserver.OnDrawListener() {
- long time = System.currentTimeMillis();
- int count = 0;
- @Override
- public void onDraw() {
- long curtime = System.currentTimeMillis();
- Log.d(TAG, "onDraw " + count++ + " " + (curtime - time) + "ms");
- time = curtime;
- if (count > 10) {
- mContainer.getViewTreeObserver().removeOnDrawListener(this);
- }
- }
- });
+ long time = System.currentTimeMillis();
+ int count = 0;
+
+ @Override
+ public void onDraw() {
+ long curtime = System.currentTimeMillis();
+ Log.d(TAG, "onDraw " + count++ + " " + (curtime - time) + "ms");
+ time = curtime;
+ if (count > 10) {
+ mContainer.getViewTreeObserver().removeOnDrawListener(this);
+ }
+ }
+ });
}
runnableAfterAnimatorReady.run();
if (mShowGuidePartial) {
@@ -529,6 +533,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
} else {
mShowAnimatorFull.start();
}
+ updateGuidePosition();
}
};
mContainer.getViewTreeObserver().addOnGlobalLayoutListener(mOnLayoutListenerForShow);
diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java
index 2fd2dac7..09a93037 100644
--- a/src/com/android/tv/guide/ProgramItemView.java
+++ b/src/com/android/tv/guide/ProgramItemView.java
@@ -37,14 +37,15 @@ import android.view.ViewGroup;
import android.widget.TextView;
import com.android.tv.ApplicationSingletons;
-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.Tracker;
+import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.Recording;
import com.android.tv.guide.ProgramManager.TableEntry;
import com.android.tv.util.Utils;
@@ -86,7 +87,7 @@ public class ProgramItemView extends TextView {
private static final View.OnClickListener ON_CLICKED = new View.OnClickListener() {
@Override
- public void onClick(View view) {
+ public void onClick(final View view) {
TableEntry entry = ((ProgramItemView) view).mTableEntry;
ApplicationSingletons singletons = TvApplication.getSingletons(view.getContext());
Tracker tracker = singletons.getTracker();
@@ -101,43 +102,82 @@ public class ProgramItemView extends TextView {
tvActivity.tuneToChannel(channel);
tvActivity.hideOverlaysForTune();
}
- }, entry.getWidth() > ((ProgramItemView) view).mMaxWidthForRipple ? 0 :
- view.getResources().getInteger(R.integer.program_guide_ripple_anim_duration));
- } else if (Features.DVR.isEnabled(view.getContext())) {
- List<CharSequence> items = new ArrayList<>();
- final List<Integer> actions = new ArrayList<>();
- // TODO: the items can be changed by the state of the program. For example,
- // if the program is already added in scheduler, we need to show an item to
- // delete the recording schedule.
- items.add(view.getResources().getString(R.string.epg_dvr_record_program));
- actions.add(ACTION_RECORD_PROGRAM);
- items.add(view.getResources().getString(R.string.epg_dvr_record_season));
- actions.add(ACTION_RECORD_SEASON);
+ }, entry.getWidth() > ((ProgramItemView) view).mMaxWidthForRipple ? 0
+ : view.getResources()
+ .getInteger(R.integer.program_guide_ripple_anim_duration));
+ } else if (CommonFeatures.DVR.isEnabled(view.getContext())) {
final MainActivity tvActivity = (MainActivity) view.getContext();
final DvrManager dvrManager = singletons.getDvrManager();
final Channel channel = tvActivity.getChannelDataManager()
.getChannel(entry.channelId);
- final Program program = entry.program;
- // TODO: it is a tentative UI. Don't publish the UI.
- new AlertDialog.Builder(view.getContext())
- .setItems(items.toArray(new CharSequence[items.size()]),
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- if (actions.get(which) == ACTION_RECORD_PROGRAM) {
- dvrManager.addSchedule(program);
- } else if (actions.get(which) == ACTION_RECORD_SEASON) {
- dvrManager.addSeasonSchedule(program);
- }
- dialog.dismiss();
- }
- })
- .create()
- .show();
+ if (dvrManager.canRecord(channel.getInputId())) {
+ showDvrDialog(view, entry, dvrManager);
+ }
}
}
+
+ private void showDvrDialog(final View view, TableEntry entry, final DvrManager dvrManager) {
+ List<CharSequence> items = new ArrayList<>();
+ final List<Integer> actions = new ArrayList<>();
+ // TODO: the items can be changed by the state of the program. For example,
+ // if the program is already added in scheduler, we need to show an item to
+ // delete the recording schedule.
+ items.add(view.getResources().getString(R.string.epg_dvr_record_program));
+ actions.add(ACTION_RECORD_PROGRAM);
+ items.add(view.getResources().getString(R.string.epg_dvr_record_season));
+ actions.add(ACTION_RECORD_SEASON);
+
+ final Program program = entry.program;
+ final List<Recording> conflicts = dvrManager
+ .getScheduledRecordingsThatConflict(program);
+ // TODO: it is a tentative UI. Don't publish the UI.
+ DialogInterface.OnClickListener onClickListener
+ = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog, int which) {
+ if (actions.get(which) == ACTION_RECORD_PROGRAM) {
+ if (conflicts.isEmpty()) {
+ dvrManager.addSchedule(program, conflicts);
+ } else {
+ showConflictDialog(view, dvrManager, program, conflicts);
+ }
+ } else if (actions.get(which) == ACTION_RECORD_SEASON) {
+ dvrManager.addSeasonSchedule(program);
+ }
+ dialog.dismiss();
+ }
+ };
+ new AlertDialog.Builder(view.getContext())
+ .setItems(items.toArray(new CharSequence[items.size()]), onClickListener)
+ .create()
+ .show();
+ }
};
+ private static void showConflictDialog(final View view, final DvrManager dvrManager,
+ final Program program, final List<Recording> conflicts) {
+ DialogInterface.OnClickListener conflictClickListener
+ = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == AlertDialog.BUTTON_POSITIVE) {
+ dvrManager.addSchedule(program, conflicts);
+ dialog.dismiss();
+ }
+ }
+ };
+ StringBuilder sb = new StringBuilder();
+ for (Recording r : conflicts) {
+ sb.append(r.toString()).append('\n');
+ }
+ new AlertDialog.Builder(view.getContext()).setTitle(R.string.dvr_epg_conflict_dialog_title)
+ .setMessage(sb.toString())
+ .setPositiveButton(R.string.dvr_epg_record, conflictClickListener)
+ .setNegativeButton(R.string.dvr_epg_do_not_record, conflictClickListener)
+ .create()
+ .show();
+ }
+
private static final View.OnFocusChangeListener ON_FOCUS_CHANGED =
new View.OnFocusChangeListener() {
@Override
@@ -218,6 +258,7 @@ public class ProgramItemView extends TextView {
@Override
protected void onFinishInflate() {
+ super.onFinishInflate();
initIfNeeded();
}
diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java
index 216fcf3c..df52abbe 100644
--- a/src/com/android/tv/guide/ProgramManager.java
+++ b/src/com/android/tv/guide/ProgramManager.java
@@ -16,14 +16,15 @@
package com.android.tv.guide;
+import android.support.annotation.MainThread;
import android.util.Log;
+import com.android.tv.common.CollectionUtils;
import com.android.tv.data.Channel;
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;
@@ -37,6 +38,7 @@ import java.util.concurrent.TimeUnit;
/**
* Manages the channels and programs for the program guide.
*/
+@MainThread
public class ProgramManager {
private static final String TAG = "ProgramManager";
private static final boolean DEBUG = false;
diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java
index cd0e9611..a86c1332 100644
--- a/src/com/android/tv/guide/ProgramTableAdapter.java
+++ b/src/com/android/tv/guide/ProgramTableAdapter.java
@@ -16,6 +16,8 @@
package com.android.tv.guide;
+import static com.android.tv.util.ImageLoader.ImageLoaderCallback;
+
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
@@ -25,6 +27,7 @@ import android.content.res.Resources;
import android.graphics.Bitmap;
import android.media.tv.TvContentRating;
import android.os.Handler;
+import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.RecycledViewPool;
import android.text.Spannable;
@@ -177,9 +180,8 @@ public class ProgramTableAdapter extends
}
// TODO: make it static
- public class ProgramRowHolder extends RecyclerView.ViewHolder implements
- ProgramRow.ChildFocusListener, Program.LoadPosterArtCallback,
- Channel.LoadImageCallback {
+ public class ProgramRowHolder extends RecyclerView.ViewHolder
+ implements ProgramRow.ChildFocusListener {
private final ViewGroup mContainer;
private final ProgramRow mProgramRow;
@@ -288,8 +290,8 @@ public class ProgramTableAdapter extends
mChannelNumberView.setText(displayNumber);
mChannelNumberView.setVisibility(View.VISIBLE);
}
- mChannelNumberView.setTextColor(isChannelLocked(channel)
- ? mChannelBlockedTextColor : mChannelTextColor);
+ mChannelNumberView.setTextColor(
+ isChannelLocked(channel) ? mChannelBlockedTextColor : mChannelTextColor);
mChannelLogoView.setImageBitmap(null);
mChannelLogoView.setVisibility(View.GONE);
@@ -302,7 +304,8 @@ public class ProgramTableAdapter extends
mChannelBlockView.setVisibility(View.GONE);
mChannel.loadBitmap(itemView.getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO,
- mChannelLogoWidth, mChannelLogoHeight, this);
+ mChannelLogoWidth, mChannelLogoHeight,
+ createChannelLogoLoadedCallback(this, channel.getId()));
}
}
@@ -386,10 +389,10 @@ public class ProgramTableAdapter extends
TvContentRating blockedRating = getProgramBlock(program);
- mImageView.setImageBitmap(null);
- mImageView.setVisibility(View.GONE);
+ updatePosterArt(null);
if (blockedRating == null) {
- program.loadPosterArt(context, mImageWidth, mImageHeight, this);
+ program.loadPosterArt(context, mImageWidth, mImageHeight,
+ createProgramPosterArtCallback(this, program));
}
if (TextUtils.isEmpty(program.getEpisodeTitle())) {
@@ -473,27 +476,12 @@ public class ProgramTableAdapter extends
}
}
- @Override
- public void onLoadPosterArtFinished(Program program, Bitmap posterArt) {
- if (posterArt == null || mSelectedEntry == null || mSelectedEntry.program == null) {
- return;
- }
-
- String posterArtUri = mSelectedEntry.program.getPosterArtUri();
- if (posterArtUri == null || !posterArtUri.equals(program.getPosterArtUri())) {
- return;
- }
-
+ private void updatePosterArt(@Nullable Bitmap posterArt) {
mImageView.setImageBitmap(posterArt);
- mImageView.setVisibility(View.VISIBLE);
+ mImageView.setVisibility(posterArt == null ? View.GONE : View.VISIBLE);
}
- @Override
- public void onLoadImageFinished(Channel channel, int type, Bitmap logo) {
- if (logo == null || mChannel == null || mChannel.getId() != channel.getId()) {
- return;
- }
-
+ private void updateChannelLogo(@Nullable Bitmap logo) {
mChannelLogoView.setImageBitmap(logo);
mChannelNameView.setVisibility(View.GONE);
mChannelLogoView.setVisibility(View.VISIBLE);
@@ -506,4 +494,36 @@ public class ProgramTableAdapter extends
}
}
}
+
+ private static ImageLoaderCallback<ProgramRowHolder> createProgramPosterArtCallback(
+ ProgramRowHolder holder, final Program program) {
+ return new ImageLoaderCallback<ProgramRowHolder>(holder) {
+ @Override
+ public void onBitmapLoaded(ProgramRowHolder holder, @Nullable Bitmap posterArt) {
+ if (posterArt == null || holder.mSelectedEntry == null
+ || holder.mSelectedEntry.program == null) {
+ return;
+ }
+ String posterArtUri = holder.mSelectedEntry.program.getPosterArtUri();
+ if (posterArtUri == null || !posterArtUri.equals(program.getPosterArtUri())) {
+ return;
+ }
+ holder.updatePosterArt(posterArt);
+ }
+ };
+ }
+
+ private static ImageLoaderCallback<ProgramRowHolder> createChannelLogoLoadedCallback(
+ ProgramRowHolder holder, final long channelId) {
+ return new ImageLoaderCallback<ProgramRowHolder>(holder) {
+ @Override
+ public void onBitmapLoaded(ProgramRowHolder holder, @Nullable Bitmap logo) {
+ if (logo == null || holder.mChannel == null
+ || holder.mChannel.getId() != channelId) {
+ return;
+ }
+ holder.updateChannelLogo(logo);
+ }
+ };
+ }
}
diff --git a/src/com/android/tv/menu/ActionCardView.java b/src/com/android/tv/menu/ActionCardView.java
index c4ddabe4..1848a3ce 100644
--- a/src/com/android/tv/menu/ActionCardView.java
+++ b/src/com/android/tv/menu/ActionCardView.java
@@ -53,6 +53,7 @@ public class ActionCardView extends FrameLayout implements ItemListRowView.CardV
@Override
protected void onFinishInflate() {
+ super.onFinishInflate();
mIconView = (ImageView) findViewById(R.id.action_card_icon);
mLabelView = (TextView) findViewById(R.id.action_card_label);
mStateView = (TextView) findViewById(R.id.action_card_state);
diff --git a/src/com/android/tv/menu/AppLinkCardView.java b/src/com/android/tv/menu/AppLinkCardView.java
index 60a4481e..74375da4 100644
--- a/src/com/android/tv/menu/AppLinkCardView.java
+++ b/src/com/android/tv/menu/AppLinkCardView.java
@@ -24,6 +24,7 @@ import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
import android.support.v7.graphics.Palette;
import android.text.TextUtils;
import android.util.AttributeSet;
@@ -37,13 +38,14 @@ import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.data.Channel;
import com.android.tv.util.BitmapUtils;
+import com.android.tv.util.ImageLoader;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
/**
* A view to render an app link card.
*/
-public class AppLinkCardView extends BaseCardView<Channel> implements Channel.LoadImageCallback {
+public class AppLinkCardView extends BaseCardView<Channel> {
private static final String TAG = MenuView.TAG;
private static final boolean DEBUG = MenuView.DEBUG;
@@ -126,7 +128,8 @@ public class AppLinkCardView extends BaseCardView<Channel> implements Channel.Lo
mAppInfoView.setText(mPackageManager.getApplicationLabel(appInfo));
if (!TextUtils.isEmpty(mChannel.getAppLinkIconUri())) {
mChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON,
- mIconWidth, mIconHeight, this);
+ mIconWidth, mIconHeight, createChannelLogoCallback(this, mChannel,
+ Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON));
} else if (appInfo.icon != 0) {
Drawable appIcon = mPackageManager.getApplicationIcon(appInfo);
BitmapUtils.setColorFilterToDrawable(mIconColorFilter, appIcon);
@@ -156,7 +159,8 @@ public class AppLinkCardView extends BaseCardView<Channel> implements Channel.Lo
if (!TextUtils.isEmpty(mChannel.getAppLinkPosterArtUri())) {
mImageView.setImageResource(R.drawable.ic_recent_thumbnail_default);
mChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART,
- mCardImageWidth, mCardImageHeight, this);
+ mCardImageWidth, mCardImageHeight, createChannelLogoCallback(this, mChannel,
+ Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART));
} else {
setCardImageWithBanner(appInfo);
}
@@ -174,12 +178,21 @@ public class AppLinkCardView extends BaseCardView<Channel> implements Channel.Lo
super.onBind(channel, selected);
}
- @Override
- public void onLoadImageFinished(Channel channel, int type, Bitmap bitmap) {
- // mChannel can be changed before the image load finished.
- if (!mChannel.hasSameReadOnlyInfo(channel)) {
- return;
- }
+ private static ImageLoader.ImageLoaderCallback<AppLinkCardView> createChannelLogoCallback(
+ AppLinkCardView cardView, final Channel channel, final int type) {
+ return new ImageLoader.ImageLoaderCallback<AppLinkCardView>(cardView) {
+ @Override
+ public void onBitmapLoaded(AppLinkCardView cardView, @Nullable Bitmap bitmap) {
+ // mChannel can be changed before the image load finished.
+ if (!cardView.mChannel.hasSameReadOnlyInfo(channel)) {
+ return;
+ }
+ cardView.updateChannelLogo(bitmap, type);
+ }
+ };
+ }
+
+ private void updateChannelLogo(@Nullable Bitmap bitmap, int type) {
if (type == Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON) {
BitmapDrawable drawable = null;
if (bitmap != null) {
@@ -188,7 +201,8 @@ public class AppLinkCardView extends BaseCardView<Channel> implements Channel.Lo
drawable.setBounds(0, 0, mIconWidth,
mIconWidth * bitmap.getHeight() / bitmap.getWidth());
} else {
- drawable.setBounds(0, 0, mIconHeight * bitmap.getWidth() / bitmap.getHeight(),
+ drawable.setBounds(0, 0,
+ mIconHeight * bitmap.getWidth() / bitmap.getHeight(),
mIconHeight);
}
}
@@ -209,6 +223,7 @@ public class AppLinkCardView extends BaseCardView<Channel> implements Channel.Lo
@Override
protected void onFinishInflate() {
+ super.onFinishInflate();
mImageView = (ImageView) findViewById(R.id.image);
mGradientView = findViewById(R.id.image_gradient);
mAppInfoView = (TextView) findViewById(R.id.app_info);
diff --git a/src/com/android/tv/menu/ChannelCardView.java b/src/com/android/tv/menu/ChannelCardView.java
index ea4f31e9..860da224 100644
--- a/src/com/android/tv/menu/ChannelCardView.java
+++ b/src/com/android/tv/menu/ChannelCardView.java
@@ -18,6 +18,7 @@ package com.android.tv.menu;
import android.content.Context;
import android.graphics.Bitmap;
+import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
@@ -32,12 +33,12 @@ import com.android.tv.R;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.parental.ParentalControlSettings;
+import com.android.tv.util.ImageLoader;
/**
* A view to render channel card.
*/
-public class ChannelCardView extends BaseCardView<Channel> implements
- Program.LoadPosterArtCallback {
+public class ChannelCardView extends BaseCardView<Channel> {
private static final String TAG = MenuView.TAG;
private static final boolean DEBUG = MenuView.DEBUG;
@@ -85,6 +86,7 @@ public class ChannelCardView extends BaseCardView<Channel> implements
@Override
protected void onFinishInflate() {
+ super.onFinishInflate();
mImageView = (ImageView) findViewById(R.id.image);
mGradientView = findViewById(R.id.image_gradient);
mChannelNumberNameView = (TextView) findViewById(R.id.channel_number_and_name);
@@ -136,13 +138,22 @@ public class ChannelCardView extends BaseCardView<Channel> implements
super.onBind(channel, selected);
}
- @Override
- public void onLoadPosterArtFinished(Program program, Bitmap posterArt) {
- if (posterArt == null || mProgram == null
- || program.getChannelId() != mProgram.getChannelId()
- || program.getChannelId() != mChannel.getId()) {
- return;
- }
+ private static ImageLoader.ImageLoaderCallback<ChannelCardView> createProgramPosterArtCallback(
+ ChannelCardView cardView, final Program program) {
+ return new ImageLoader.ImageLoaderCallback<ChannelCardView>(cardView) {
+ @Override
+ public void onBitmapLoaded(ChannelCardView cardView, @Nullable Bitmap posterArt) {
+ if (posterArt == null || cardView.mProgram == null
+ || program.getChannelId() != cardView.mProgram.getChannelId()
+ || program.getChannelId() != cardView.mChannel.getId()) {
+ return;
+ }
+ cardView.updatePosterArt(posterArt);
+ }
+ };
+ }
+
+ private void updatePosterArt(Bitmap posterArt) {
mImageView.setImageBitmap(posterArt);
mGradientView.setVisibility(View.VISIBLE);
}
@@ -208,7 +219,7 @@ public class ChannelCardView extends BaseCardView<Channel> implements
|| !parental.isRatingBlocked(mProgram.getContentRatings()))
&& !TextUtils.isEmpty(mProgram.getPosterArtUri())) {
mProgram.loadPosterArt(getContext(), mCardImageWidth, mCardImageHeight,
- ChannelCardView.this);
+ createProgramPosterArtCallback(this, mProgram));
}
}
diff --git a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java
index d576342c..1e416e5b 100644
--- a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java
+++ b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java
@@ -18,7 +18,9 @@ package com.android.tv.menu;
import android.content.Context;
import android.os.Handler;
+import android.os.Looper;
import android.os.Message;
+import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.util.Log;
@@ -49,7 +51,6 @@ public class ChannelsPosterPrefetcher {
private boolean isCanceled;
-
/**
* Create {@link ChannelsPosterPrefetcher} object with given parameters.
*/
@@ -72,16 +73,14 @@ public class ChannelsPosterPrefetcher {
if (isCanceled) {
return;
}
- if (DEBUG) {
- Log.d(TAG, "startPrefetching()");
- }
+ if (DEBUG) Log.d(TAG, "startPrefetching()");
/*
* When a user browse channels, this method could be called many times. We don't need to
* prefetch the intermediate channels. So ignore previous schedule.
*/
mHandler.removeMessages(MSG_PREFETCH_IMAGE);
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(MSG_PREFETCH_IMAGE), ONDEMAND_POSTER_PREFETCH_DELAY_MILLIS);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_PREFETCH_IMAGE),
+ ONDEMAND_POSTER_PREFETCH_DELAY_MILLIS);
}
/**
@@ -92,11 +91,12 @@ public class ChannelsPosterPrefetcher {
mHandler.removeCallbacksAndMessages(null);
}
+ @MainThread // ProgramDataManager.getCurrentProgram must be called from the main thread
private void doPrefetchImages() {
- if (DEBUG) {
- Log.d(TAG, "doPrefetchImages()");
- }
+ if (DEBUG) Log.d(TAG, "doPrefetchImages() started");
+ // This executes on the main thread, but since the item list is expected to be about 5 items
+ // and ImageLoader spawns an async task so this is fast enough. 1 ms in local testing.
List<Channel> channelList = mChannelsAdapter.getItemList();
if (channelList != null) {
for (Channel channel : channelList) {
@@ -114,14 +114,20 @@ public class ChannelsPosterPrefetcher {
}
}
}
+ if (DEBUG) {
+ Log.d(TAG, "doPrefetchImages() finished. ImageLoader may still have async tasks for "
+ + "channels " + channelList);
+ }
}
private static class PrefetchHandler extends WeakHandler<ChannelsPosterPrefetcher> {
public PrefetchHandler(ChannelsPosterPrefetcher ref) {
- super(ref);
+ // doPrefetchImages must be called from the main thread.
+ super(Looper.getMainLooper(), ref);
}
@Override
+ @MainThread
public void handleMessage(Message msg, @NonNull ChannelsPosterPrefetcher prefetcher) {
switch (msg.what) {
case MSG_PREFETCH_IMAGE:
diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java
index 6cbd67ce..51867d0b 100644
--- a/src/com/android/tv/menu/ChannelsRowAdapter.java
+++ b/src/com/android/tv/menu/ChannelsRowAdapter.java
@@ -25,6 +25,7 @@ import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.analytics.Tracker;
+import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
import com.android.tv.recommendation.Recommender;
import com.android.tv.util.SetupUtils;
@@ -36,16 +37,16 @@ import java.util.List;
* An adapter of the Channels row.
*/
public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel> {
- private static final int POSITION_FIRST_CARD = 0;
- private static final int POSITION_SECOND_CARD = 1;
- private static final int POSITION_THIRD_CARD = 2;
+ // There are four special cards: guide, setup, dvr, applink.
+ private static final int SIZE_OF_VIEW_TYPE = 4;
+
private final Context mContext;
private final Tracker mTracker;
private final Recommender mRecommender;
private final int mMaxCount;
private final int mMinCount;
- private boolean mShowSetupCard;
- private boolean mShowAppLinkCard;
+ private boolean mShowDvrCard;
+ private int[] mViewType = new int[SIZE_OF_VIEW_TYPE];
private final View.OnClickListener mGuideOnClickListener = new View.OnClickListener() {
@Override
@@ -59,7 +60,15 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
@Override
public void onClick(View view) {
mTracker.sendMenuClicked(R.string.channels_item_setup);
- getMainActivity().getOverlayManager().showSetupDialog();
+ getMainActivity().getOverlayManager().showSetupFragment();
+ }
+ };
+
+ private final View.OnClickListener mDvrOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mTracker.sendMenuClicked(R.string.channels_item_dvr);
+ getMainActivity().getOverlayManager().showDvrManager();
}
};
@@ -93,26 +102,15 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
mRecommender = recommender;
mMinCount = minCount;
mMaxCount = maxCount;
+ mShowDvrCard = CommonFeatures.DVR.isEnabled(mContext);
}
@Override
public int getItemViewType(int position) {
- switch (position) {
- case POSITION_FIRST_CARD:
- return R.layout.menu_card_guide;
- case POSITION_SECOND_CARD:
- return mShowSetupCard
- ? R.layout.menu_card_setup
- : mShowAppLinkCard
- ? R.layout.menu_card_app_link
- : R.layout.menu_card_channel;
- case POSITION_THIRD_CARD:
- return (mShowSetupCard && mShowAppLinkCard)
- ? R.layout.menu_card_app_link
- : R.layout.menu_card_channel;
- default:
- return R.layout.menu_card_channel;
+ if (position >= SIZE_OF_VIEW_TYPE) {
+ return R.layout.menu_card_channel;
}
+ return mViewType[position];
}
@Override
@@ -131,6 +129,8 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
viewHolder.itemView.setOnClickListener(mSetupOnClickListener);
} else if (viewType == R.layout.menu_card_app_link) {
viewHolder.itemView.setOnClickListener(mAppLinkOnClickListener);
+ } else if (viewType == R.layout.menu_card_dvr) {
+ viewHolder.itemView.setOnClickListener(mDvrOnClickListener);
} else {
viewHolder.itemView.setTag(getItemList().get(position));
viewHolder.itemView.setOnClickListener(mChannelOnClickListener);
@@ -145,20 +145,30 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
// For guide item
channelList.add(dummyChannel);
// For setup item
- mShowSetupCard = SetupUtils.getInstance(mContext).hasNewInput(
- ((MainActivity) mContext).getTvInputManagerHelper());
- if (mShowSetupCard) {
+ boolean showSetupCard = SetupUtils.getInstance(mContext)
+ .hasNewInput(((MainActivity) mContext).getTvInputManagerHelper());
+ Channel currentChannel = ((MainActivity) mContext).getCurrentChannel();
+ boolean showAppLinkCard = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ && currentChannel != null
+ && currentChannel.getAppLinkType(mContext) != Channel.APP_LINK_TYPE_NONE;
+
+ mViewType[0] = R.layout.menu_card_guide;
+ int index = 1;
+ if (showSetupCard) {
channelList.add(dummyChannel);
+ mViewType[index++] = R.layout.menu_card_setup;
}
- if (Build.VERSION.SDK_INT >= 23) {
- Channel currentChannel = ((MainActivity) mContext).getCurrentChannel();
- mShowAppLinkCard = currentChannel != null
- && currentChannel.getAppLinkType(mContext) != Channel.APP_LINK_TYPE_NONE;
- if (mShowAppLinkCard) {
- channelList.add(currentChannel);
- }
+ if (mShowDvrCard) {
+ channelList.add(dummyChannel);
+ mViewType[index++] = R.layout.menu_card_dvr;
+ }
+ if (showAppLinkCard) {
+ channelList.add(currentChannel);
+ mViewType[index++] = R.layout.menu_card_app_link;
+ }
+ for ( ; index < mViewType.length; ++index) {
+ mViewType[index] = R.layout.menu_card_channel;
}
-
channelList.addAll(getRecentChannels());
setItemList(channelList);
}
diff --git a/src/com/android/tv/menu/MenuAction.java b/src/com/android/tv/menu/MenuAction.java
index 5ef714dd..b45e88c2 100644
--- a/src/com/android/tv/menu/MenuAction.java
+++ b/src/com/android/tv/menu/MenuAction.java
@@ -42,18 +42,13 @@ public class MenuAction {
public static final MenuAction SELECT_AUDIO_LANGUAGE_ACTION =
new MenuAction(R.string.options_item_multi_audio, TvOptionsManager.OPTION_MULTI_AUDIO,
R.drawable.ic_tvoption_multi_track);
- public static final MenuAction CHANNEL_SOURCES_ACTION =
- new MenuAction(R.string.options_item_channel_sources,
- TvOptionsManager.OPTION_CHANNEL_SOURCES,
- R.drawable.ic_tvoption_channel_sources);
- public static final MenuAction PARENTAL_CONTROLS_ACTION =
- new MenuAction(R.string.options_item_parental_controls,
- TvOptionsManager.OPTION_PARENTAL_CONTROLS,
- R.drawable.ic_tvoption_parental);
- public static final MenuAction ABOUT_ACTION =
- new MenuAction(R.string.options_item_about,
- TvOptionsManager.OPTION_ABOUT,
- R.drawable.ic_tvoption_about);
+ public static final MenuAction MORE_CHANNELS_ACTION =
+ new MenuAction(R.string.options_item_more_channels,
+ TvOptionsManager.OPTION_MORE_CHANNELS, R.drawable.ic_store);
+ // TODO: Change the icon.
+ public static final MenuAction SETTINGS_ACTION =
+ new MenuAction(R.string.options_item_settings, TvOptionsManager.OPTION_SETTINGS,
+ R.drawable.ic_settings);
// Actions in the PIP option row.
public static final MenuAction PIP_SELECT_INPUT_ACTION =
new MenuAction(R.string.pip_options_item_source, TvOptionsManager.OPTION_PIP_INPUT,
diff --git a/src/com/android/tv/menu/MenuRowView.java b/src/com/android/tv/menu/MenuRowView.java
index 6b3b6b5f..7cdbfe9e 100644
--- a/src/com/android/tv/menu/MenuRowView.java
+++ b/src/com/android/tv/menu/MenuRowView.java
@@ -115,6 +115,7 @@ public abstract class MenuRowView extends LinearLayout {
@Override
protected void onFinishInflate() {
+ super.onFinishInflate();
mTitleView = (TextView) findViewById(R.id.title);
mContentsView = findViewById(getContentsViewId());
if (mContentsView.isFocusable()) {
diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java
index d4ad7877..f0853c40 100644
--- a/src/com/android/tv/menu/PlayControlsRowView.java
+++ b/src/com/android/tv/menu/PlayControlsRowView.java
@@ -54,6 +54,7 @@ public class PlayControlsRowView extends MenuRowView {
private View mUnavailableMessageText;
private TimeShiftManager mTimeShiftManager;
+ private final java.text.DateFormat mTimeFormat;
private long mProgramStartTimeMs;
private long mProgramEndTimeMs;
@@ -78,6 +79,7 @@ public class PlayControlsRowView extends MenuRowView {
mTimeTextLeftMargin =
- res.getDimensionPixelOffset(R.dimen.play_controls_time_width) / 2;
mTimelineWidth = res.getDimensionPixelSize(R.dimen.play_controls_width);
+ mTimeFormat = DateFormat.getTimeFormat(context);
}
@Override
@@ -188,7 +190,7 @@ public class PlayControlsRowView extends MenuRowView {
}
@Override
- public void onRecordStartTimeChanged() {
+ public void onRecordTimeRangeChanged() {
if (!mTimeShiftManager.isAvailable()) {
return;
}
@@ -355,6 +357,14 @@ public class PlayControlsRowView extends MenuRowView {
mTimeIndicator.setVisibility(View.INVISIBLE);
return;
}
+ if (mTimeShiftManager.isPlayForRecording()) {
+ mProgramStartTimeMs = mTimeShiftManager.getRecordStartTimeMs();
+ mProgramEndTimeMs = Math.max(mProgramStartTimeMs,
+ mTimeShiftManager.getRecordEndTimeMs());
+ if (mProgramStartTimeMs > mProgramEndTimeMs) {
+ mProgramEndTimeMs = mProgramStartTimeMs;
+ }
+ }
long currentPositionMs = mTimeShiftManager.getCurrentPositionMs();
ViewGroup.MarginLayoutParams params =
(ViewGroup.MarginLayoutParams) mTimeText.getLayoutParams();
@@ -387,11 +397,11 @@ public class PlayControlsRowView extends MenuRowView {
}
long progressStartTimeMs = Math.min(mProgramEndTimeMs,
- Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordStartTimeMs()));
+ Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordStartTimeMs()));
long currentPlayingTimeMs = Math.min(mProgramEndTimeMs,
- Math.max(mProgramStartTimeMs, mTimeShiftManager.getCurrentPositionMs()));
+ Math.max(mProgramStartTimeMs, mTimeShiftManager.getCurrentPositionMs()));
long progressEndTimeMs = Math.min(mProgramEndTimeMs,
- Math.max(mProgramStartTimeMs, System.currentTimeMillis()));
+ Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordEndTimeMs()));
layoutProgress(mProgressEmptyBefore, mProgramStartTimeMs, progressStartTimeMs);
layoutProgress(mProgressWatched, progressStartTimeMs, currentPlayingTimeMs);
@@ -468,7 +478,7 @@ public class PlayControlsRowView extends MenuRowView {
}
private String getTimeString(long timeMs) {
- return DateFormat.getTimeFormat(getContext()).format(timeMs);
+ return mTimeFormat.format(timeMs);
}
private int convertDurationToPixel(long duration) {
diff --git a/src/com/android/tv/menu/GuideCardView.java b/src/com/android/tv/menu/SimpleCardView.java
index 94d625bd..24a44244 100644
--- a/src/com/android/tv/menu/GuideCardView.java
+++ b/src/com/android/tv/menu/SimpleCardView.java
@@ -25,20 +25,20 @@ import com.android.tv.data.Channel;
/**
* A view to render a guide card.
*/
-public class GuideCardView extends BaseCardView<Channel> {
+public class SimpleCardView extends BaseCardView<Channel> {
private static final String TAG = "GuideCardView";
private static final boolean DEBUG = false;
private final float mCardHeight;
- public GuideCardView(Context context) {
+ public SimpleCardView(Context context) {
this(context, null, 0);
}
- public GuideCardView(Context context, AttributeSet attrs) {
+ public SimpleCardView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
- public GuideCardView(Context context, AttributeSet attrs, int defStyle) {
+ public SimpleCardView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mCardHeight = getResources().getDimension(R.dimen.card_layout_height);
}
diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java
index 1977dde1..82525456 100644
--- a/src/com/android/tv/menu/TvOptionsRowAdapter.java
+++ b/src/com/android/tv/menu/TvOptionsRowAdapter.java
@@ -20,16 +20,15 @@ import android.content.Context;
import android.media.tv.TvTrackInfo;
import android.support.annotation.VisibleForTesting;
+import com.android.tv.Features;
import com.android.tv.R;
import com.android.tv.TvOptionsManager;
import com.android.tv.customization.CustomAction;
import com.android.tv.data.DisplayMode;
import com.android.tv.ui.TvViewUiManager;
-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;
@@ -50,25 +49,18 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
protected List<MenuAction> createBaseActions() {
List<MenuAction> actionList = new ArrayList<>();
actionList.add(MenuAction.SELECT_CLOSED_CAPTION_ACTION);
+ setOptionChangedListener(MenuAction.SELECT_CLOSED_CAPTION_ACTION);
actionList.add(MenuAction.SELECT_DISPLAY_MODE_ACTION);
+ setOptionChangedListener(MenuAction.SELECT_DISPLAY_MODE_ACTION);
actionList.add(MenuAction.PIP_ACTION);
+ setOptionChangedListener(MenuAction.PIP_ACTION);
mPositionPipAction = actionList.size() - 1;
actionList.add(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION);
- actionList.add(MenuAction.CHANNEL_SOURCES_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) {
- if (action.getType() != TvOptionsManager.OPTION_CHANNEL_SOURCES) {
- setOptionChangedListener(action);
- }
+ setOptionChangedListener(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION);
+ if (Features.ONBOARDING_PLAY_STORE.isEnabled(getMainActivity())) {
+ actionList.add(MenuAction.MORE_CHANNELS_ACTION);
}
+ actionList.add(MenuAction.SETTINGS_ACTION);
if (getCustomActions() != null) {
// Adjust Pip action position which will be changed by applying custom actions.
@@ -188,15 +180,11 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
getMainActivity().getOverlayManager().getSideFragmentManager().show(
new MultiAudioFragment());
break;
- case TvOptionsManager.OPTION_CHANNEL_SOURCES:
- getMainActivity().showChannelSourcesFragment();
- break;
- case TvOptionsManager.OPTION_PARENTAL_CONTROLS:
- getMainActivity().showParentalControlFragment();
+ case TvOptionsManager.OPTION_MORE_CHANNELS:
+ getMainActivity().showMerchantCollection();
break;
- case TvOptionsManager.OPTION_ABOUT:
- getMainActivity().getOverlayManager().getSideFragmentManager().show(
- new AboutFragment());
+ case TvOptionsManager.OPTION_SETTINGS:
+ getMainActivity().showSettingsFragment();
break;
}
}
diff --git a/src/com/android/tv/onboarding/AppOverviewFragment.java b/src/com/android/tv/onboarding/AppOverviewFragment.java
deleted file mode 100644
index a2f5d768..00000000
--- a/src/com/android/tv/onboarding/AppOverviewFragment.java
+++ /dev/null
@@ -1,110 +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.onboarding;
-
-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.Features;
-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 SetupGuidedStepFragment onCreateContentFragment() {
- 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.getSingletons(getActivity()).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());
- }
- if (Features.ONBOARDING_PLAY_STORE.isEnabled(getActivity())) {
- 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 (Features.ONBOARDING_USB_TUNER.isEnabled(getActivity()) && 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/NewSourcesFragment.java b/src/com/android/tv/onboarding/NewSourcesFragment.java
new file mode 100644
index 00000000..ebaf0b6c
--- /dev/null
+++ b/src/com/android/tv/onboarding/NewSourcesFragment.java
@@ -0,0 +1,82 @@
+/*
+ * 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.Build;
+import android.os.Bundle;
+import android.transition.Slide;
+import android.view.Gravity;
+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.OnActionClickListener;
+import com.android.tv.common.ui.setup.SetupActionHelper;
+import com.android.tv.util.SetupUtils;
+
+/**
+ * A fragment for new channel source info/setup.
+ */
+public class NewSourcesFragment extends Fragment {
+ public static final String ACTION_CATEOGRY = NewSourcesFragment.class.getCanonicalName();
+ public static final int ACTION_SETUP = 1;
+ public static final int ACTION_SKIP = 2;
+
+ private OnActionClickListener mOnActionClickListener;
+
+ public NewSourcesFragment() {
+ setAllowEnterTransitionOverlap(false);
+ setAllowReturnTransitionOverlap(false);
+ setEnterTransition(new Slide(Gravity.BOTTOM));
+ setExitTransition(new Slide(Gravity.BOTTOM));
+ setReenterTransition(new Slide(Gravity.BOTTOM));
+ setReturnTransition(new Slide(Gravity.BOTTOM));
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_new_sources, container, false);
+ initializeButton(view.findViewById(R.id.setup), ACTION_SETUP);
+ initializeButton(view.findViewById(R.id.skip), ACTION_SKIP);
+ SetupUtils.getInstance(getActivity()).markAllInputsRecognized(TvApplication
+ .getSingletons(getActivity()).getTvInputManagerHelper());
+ view.requestFocus();
+ return view;
+ }
+
+ /**
+ * Sets the {@link OnActionClickListener}. This method should be called before the views are
+ * created.
+ */
+ public void setOnActionClickListener(OnActionClickListener onActionClickListener) {
+ mOnActionClickListener = onActionClickListener;
+ }
+
+ private void initializeButton(View view, int actionId) {
+ view.setOnClickListener(SetupActionHelper.createOnClickListenerForAction(
+ mOnActionClickListener, ACTION_CATEOGRY, actionId));
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ // Prior to M, foreground ripple animation is not supported.
+ // Use background ripple drawable instead of drawing in the foreground manually.
+ view.setBackground(getActivity().getDrawable(R.drawable.setup_selector_background));
+ }
+ }
+}
diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java
index 3717a611..3ae80597 100644
--- a/src/com/android/tv/onboarding/OnboardingActivity.java
+++ b/src/com/android/tv/onboarding/OnboardingActivity.java
@@ -17,48 +17,45 @@
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.net.Uri;
+import android.content.pm.PackageManager;
+import android.os.Build;
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.TvApplication;
-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.common.ui.setup.SetupActivity;
+import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
import com.android.tv.data.ChannelDataManager;
-import com.android.tv.receiver.AudioCapabilitiesReceiver;
import com.android.tv.util.OnboardingUtils;
+import com.android.tv.util.PermissionUtils;
import com.android.tv.util.SetupUtils;
-import com.android.tv.util.SoftPreconditions;
-import com.android.tv.util.Utils;
-import java.util.Locale;
-import java.util.concurrent.TimeUnit;
+public class OnboardingActivity extends SetupActivity {
+ private static final String KEY_INTENT_AFTER_COMPLETION = "key_intent_after_completion";
-public class OnboardingActivity extends SteppedSetupActivity {
- private static final String TAG = "OnboardingActivity";
+ private static final int PERMISSIONS_REQUEST_READ_TV_LISTINGS = 1;
+ private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
- private static final String KEY_INTENT_AFTER_COMPLETION = "key_intent_after_completion";
+ private static final int SHOW_RIPPLE_DURATION_MS = 266;
- private static final int MSG_CHECK_RECEIVED_AC3_CAPABILITY_NOTIFICATION = 1;
- private static final long AC3_CHECK_WAIT_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
+ private ChannelDataManager mChannelDataManager;
+ private final ChannelDataManager.Listener mChannelListener = new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ mChannelDataManager.removeListener(this);
+ SetupUtils.getInstance(OnboardingActivity.this).markNewChannelsBrowsable();
+ }
- private static final int REQUEST_CODE_SETUP_USB_TUNER = 1;
+ @Override
+ public void onChannelListUpdated() { }
- private Handler mHandler = new OnboardingActivityHandler(this);
- private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
- private Boolean mAc3Supported;
+ @Override
+ public void onChannelBrowsableChanged() { }
+ };
/**
* Returns an intent to start {@link OnboardingActivity}.
@@ -76,72 +73,51 @@ public class OnboardingActivity extends SteppedSetupActivity {
@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);
+ // Make the channels of the new inputs which have been setup outside Live TV
+ // browsable.
+ mChannelDataManager = TvApplication.getSingletons(this).getChannelDataManager();
+ if (mChannelDataManager.isDbLoadFinished()) {
+ SetupUtils.getInstance(this).markNewChannelsBrowsable();
+ } else {
+ mChannelDataManager.addListener(mChannelListener);
}
- return new AppOverviewStep(null);
}
@Override
protected void onDestroy() {
- mHandler.removeCallbacksAndMessages(null);
- if (mAudioCapabilitiesReceiver != null) {
- mAudioCapabilitiesReceiver.unregister();
- mAudioCapabilitiesReceiver = null;
- }
+ mChannelDataManager.removeListener(mChannelListener);
super.onDestroy();
}
- void startFirstStep() {
- SoftPreconditions.checkNotNull(mAc3Supported, TAG,
- "AC3 passthrough support check hasn't been completed yet.");
- startInitialStep();
+ @Override
+ protected Fragment onCreateInitialFragment() {
+ return OnboardingUtils.isFirstRunWithCurrentVersion(this) ? new WelcomeFragment()
+ : new SetupSourcesFragment();
}
@Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (requestCode == REQUEST_CODE_SETUP_USB_TUNER && resultCode == RESULT_OK) {
- SetupUtils.getInstance(this).onTvInputSetupFinished(Utils.getUsbTunerInputId(this),
- null);
- return;
+ protected void 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);
+ }
}
- super.onActivityResult(requestCode, resultCode, data);
}
- 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();
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ if (requestCode == PERMISSIONS_REQUEST_READ_TV_LISTINGS) {
+ if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
+ Toast.makeText(this, R.string.msg_read_tv_listing_permission_denied,
+ Toast.LENGTH_LONG).show();
+ finish();
}
}
}
@@ -155,112 +131,44 @@ public class OnboardingActivity extends SteppedSetupActivity {
finish();
}
- private class WelcomeStep extends SetupStep {
- public WelcomeStep(@Nullable SetupStep previousStep) {
- super(getFragmentManager(), previousStep);
- }
-
- @Override
- public Fragment onCreateFragment() {
- return new WelcomeFragment();
- }
-
- @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), false);
- } else {
- // TODO: Go to the correct step.
- finishActivity();
- }
- break;
+ void showMerchantCollection() {
+ executeActionWithDelay(new Runnable() {
+ @Override
+ public void run() {
+ startActivity(OnboardingUtils.PLAY_STORE_INTENT);
}
- }
+ }, SHOW_RIPPLE_DURATION_MS);
}
- private class AppOverviewStep extends SetupStep {
- private static final String TV_MERCHANT_COLLECTION = "https://play.google.com/store/apps/"
- + "collection/promotion_3001bf9_ATV_livechannels?sticky_source_country=";
-
- 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
- public void executeAction(int actionId) {
- switch (actionId) {
- case AppOverviewFragment.ACTION_SETUP_SOURCE: {
- startStep(new SetupSourcesStep(this), true);
- break;
+ @Override
+ protected void executeAction(String category, int actionId) {
+ switch (category) {
+ case WelcomeFragment.ACTION_CATEGORY:
+ switch (actionId) {
+ case WelcomeFragment.ACTION_NEXT:
+ OnboardingUtils.setFirstRunWithCurrentVersionCompleted(
+ OnboardingActivity.this);
+ showFragment(new SetupSourcesFragment(), false);
+ break;
}
- case AppOverviewFragment.ACTION_GET_MORE_CHANNELS:
- startActivity(new Intent(Intent.ACTION_VIEW,
- Uri.parse(TV_MERCHANT_COLLECTION + Locale.getDefault().getCountry())));
- break;
- case AppOverviewFragment.ACTION_SETUP_USB_TUNER: {
- Context context = OnboardingActivity.this;
- TvInputInfo input = Utils.getUsbTunerInputInfo(context);
- if (input != null) {
- SetupUtils.grantEpgPermission(context,
- input.getServiceInfo().packageName);
- Intent intent = input.createSetupIntent();
- try {
- startActivityForResult(intent, REQUEST_CODE_SETUP_USB_TUNER);
- } catch (ActivityNotFoundException e) {
- Toast.makeText(context, getString(
- R.string.msg_unable_to_start_setup_activity,
- input.loadLabel(context)), Toast.LENGTH_SHORT).show();
- Log.e(TAG, "Can't find activity: " + intent.getComponent(), e);
- break;
+ break;
+ case SetupSourcesFragment.ACTION_CATEGORY:
+ switch (actionId) {
+ case SetupSourcesFragment.ACTION_PLAY_STORE:
+ showMerchantCollection();
+ break;
+ case SetupMultiPaneFragment.ACTION_DONE: {
+ ChannelDataManager manager = TvApplication.getSingletons(
+ OnboardingActivity.this).getChannelDataManager();
+ if (manager.getChannelCount() == 0) {
+ finish();
+ } else {
+ finishActivity();
}
- // TODO: Add transition animation.
- } else {
- // TODO: Implement this.
- Toast.makeText(OnboardingActivity.this, "Not implemented yet.",
- Toast.LENGTH_SHORT).show();
+ break;
}
- 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: {
- ChannelDataManager manager = TvApplication.getSingletons(
- OnboardingActivity.this).getChannelDataManager();
- if (manager.getChannelCount() == 0) {
- finish();
- } else {
- finishActivity();
- }
- break;
- }
- }
+ break;
}
}
}
diff --git a/src/com/android/tv/onboarding/PagingIndicator.java b/src/com/android/tv/onboarding/PagingIndicator.java
deleted file mode 100644
index 107b00f0..00000000
--- a/src/com/android/tv/onboarding/PagingIndicator.java
+++ /dev/null
@@ -1,235 +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.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_fragment_transition_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
index ad49dc23..ebf32d00 100644
--- a/src/com/android/tv/onboarding/SetupSourcesFragment.java
+++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java
@@ -20,19 +20,26 @@ import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputManager.TvInputCallback;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
+import android.support.v17.leanback.widget.GuidedActionsStylist;
import android.support.v17.leanback.widget.VerticalGridView;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
import android.widget.Toast;
import com.android.tv.ApplicationSingletons;
+import com.android.tv.Features;
import com.android.tv.R;
import com.android.tv.SetupPassthroughActivity;
import com.android.tv.TvApplication;
@@ -43,6 +50,7 @@ import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.TvInputNewComparator;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.TvInputManagerHelper;
+import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.Collections;
@@ -52,70 +60,187 @@ import java.util.List;
* A fragment for channel source info/setup.
*/
public class SetupSourcesFragment extends SetupMultiPaneFragment {
+ public static final String ACTION_CATEGORY =
+ "com.android.tv.onboarding.SetupSourcesFragment";
+ public static final int ACTION_PLAY_STORE = 1;
+
+ public static final int DEFAULT_THEME = -1;
+
+ private static final String SETUP_TRACKER_LABEL = "Setup fragment";
+
+ private static int sTheme = DEFAULT_THEME;
+
+ private InputSetupRunnable mInputSetupRunnable;
+
+ private ContentFragment mContentFragment;
+
@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);
+ LayoutInflater localInflater = inflater;
+ if (sTheme != -1) {
+ ContextThemeWrapper themeWrapper = new ContextThemeWrapper(getActivity(), sTheme);
+ localInflater = inflater.cloneInContext(themeWrapper);
+ }
+ View view = super.onCreateView(localInflater, container, savedInstanceState);
+ TvApplication.getSingletons(getActivity()).getTracker().sendScreenView(SETUP_TRACKER_LABEL);
return view;
}
@Override
+ protected void onEnterTransitionEnd() {
+ if (mContentFragment != null) {
+ mContentFragment.executePendingAction();
+ }
+ }
+
+ @Override
protected SetupGuidedStepFragment onCreateContentFragment() {
- SetupGuidedStepFragment fragment = new ContentFragment(getActivity());
+ mContentFragment = new ContentFragment();
Bundle arguments = new Bundle();
arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true);
- fragment.setArguments(arguments);
- return fragment;
+ mContentFragment.setArguments(arguments);
+ mContentFragment.setParentFragment(this);
+ return mContentFragment;
+ }
+
+ @Override
+ protected String getActionCategory() {
+ return ACTION_CATEGORY;
}
- private class ContentFragment extends SetupGuidedStepFragment {
+ /**
+ * Sets the custom theme dynamically.
+ */
+ public static void setTheme(int theme) {
+ sTheme = theme;
+ }
+
+ /**
+ * Call this method to run customized input setup.
+ *
+ * @param runnable runnable to be called when the input setup is necessary.
+ */
+ public void setInputSetupRunnable(InputSetupRunnable runnable) {
+ mInputSetupRunnable = runnable;
+ }
+
+ /**
+ * Interface for the customized input setup.
+ */
+ public interface InputSetupRunnable {
+ /**
+ * Called for the input setup.
+ *
+ * @param input TV input for setup.
+ */
+ void runInputSetup(TvInputInfo input);
+ }
+
+ public static class ContentFragment extends SetupGuidedStepFragment {
private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1;
- private static final int ACTION_DIVIDER = ACTION_DONE + 1;
- private static final int ACTION_INPUT_START = ACTION_DONE + 2;
+ // ACTION_PLAY_STORE is defined in the outer class.
+ private static final int ACTION_DIVIDER = 2;
+ private static final int ACTION_HEADER = 3;
+ private static final int ACTION_INPUT_START = 4;
+
+ private static final int PENDING_ACTION_NONE = 0;
+ private static final int PENDING_ACTION_INPUT_CHANGED = 1;
+ private static final int PENDING_ACTION_CHANNEL_CHANGED = 2;
- private final TvInputManagerHelper mInputManager;
- private final ChannelDataManager mChannelDataManager;
- private final SetupUtils mSetupUtils;
- private List<TvInputInfo> mInputList;
- private SetupSourcesAdapter mAdapter;
+ private TvInputManagerHelper mInputManager;
+ private ChannelDataManager mChannelDataManager;
+ private SetupUtils mSetupUtils;
+ private List<TvInputInfo> mInputs;
private int mKnownInputStartIndex;
- private boolean mShowDivider;
+ private int mDoneInputStartIndex;
+
+ private SetupSourcesFragment mParentFragment;
+
+ private String mNewlyAddedInputId;
+
+ private int mPendingAction = PENDING_ACTION_NONE;
+
+ private final TvInputCallback mInputCallback = new TvInputCallback() {
+ @Override
+ public void onInputAdded(String inputId) {
+ handleInputChanged();
+ }
+
+ @Override
+ public void onInputRemoved(String inputId) {
+ handleInputChanged();
+ }
+
+ private void handleInputChanged() {
+ // The actions created while enter transition is running will not be included in the
+ // fragment transition.
+ if (mParentFragment.isEnterTransitionRunning()) {
+ mPendingAction = PENDING_ACTION_INPUT_CHANGED;
+ return;
+ }
+ buildInputs();
+ updateActions();
+ }
+ };
+
+ void setParentFragment(SetupSourcesFragment parentFragment) {
+ mParentFragment = parentFragment;
+ }
+
+ private final ChannelDataManager.Listener mChannelDataManagerListener
+ = new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ handleChannelChanged();
+ }
+
+ @Override
+ public void onChannelListUpdated() {
+ handleChannelChanged();
+ }
- ContentFragment(Context context) {
+ @Override
+ public void onChannelBrowsableChanged() {
+ handleChannelChanged();
+ }
+
+ private void handleChannelChanged() {
+ // The actions created while enter transition is running will not be included in the
+ // fragment transition.
+ if (mParentFragment.isEnterTransitionRunning()) {
+ if (mPendingAction != PENDING_ACTION_INPUT_CHANGED) {
+ mPendingAction = PENDING_ACTION_CHANNEL_CHANGED;
+ }
+ return;
+ }
+ updateActions();
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
// TODO: Handle USB TV tuner differently.
+ Context context = getActivity();
ApplicationSingletons app = TvApplication.getSingletons(context);
mInputManager = app.getTvInputManagerHelper();
mChannelDataManager = app.getChannelDataManager();
mSetupUtils = SetupUtils.getInstance(context);
- mInputList = mInputManager.getTvInputInfos(true, true);
- Collections.sort(mInputList, new TvInputNewComparator(mSetupUtils, mInputManager));
- mKnownInputStartIndex = 0;
- for (TvInputInfo input : mInputList) {
- if (mSetupUtils.isNewInput(input.getId())) {
- mSetupUtils.markAsKnownInput(input.getId());
- ++mKnownInputStartIndex;
- }
- }
- mShowDivider = mKnownInputStartIndex != 0 && mKnownInputStartIndex != mInputList.size();
- if (mAdapter != null) {
- mAdapter.notifyDataSetChanged();
- }
+ buildInputs();
+ mInputManager.addCallback(mInputCallback);
+ mChannelDataManager.addListener(mChannelDataManagerListener);
+ super.onCreate(savedInstanceState);
}
- @SuppressWarnings("rawtypes")
@Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View view = super.onCreateView(inflater, container, savedInstanceState);
- VerticalGridView gridView = getGuidedActionsStylist().getActionsGridView();
- RecyclerView.Adapter adapter = gridView.getAdapter();
- mAdapter = new SetupSourcesAdapter(adapter);
- gridView.setAdapter(mAdapter);
- return view;
+ public void onDestroy() {
+ super.onDestroy();
+ mChannelDataManager.removeListener(mChannelDataManagerListener);
+ mInputManager.removeCallback(mInputCallback);
}
+ @NonNull
@Override
public Guidance onCreateGuidance(Bundle savedInstanceState) {
String title = getString(R.string.setup_sources_text);
@@ -124,22 +249,42 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
}
@Override
- public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- createActionsInternal(actions);
- if (!mChannelDataManager.isDbLoadFinished()) {
- mChannelDataManager.addListener(new ChannelDataManager.Listener() {
- @Override
- public void onLoadFinished() {
- mChannelDataManager.removeListener(this);
- updateActions();
- }
+ public GuidedActionsStylist onCreateActionsStylist() {
+ return new SetupSourceGuidedActionsStylist();
+ }
- @Override
- public void onChannelListUpdated() { }
+ @Override
+ public void onCreateActions(@NonNull List<GuidedAction> actions,
+ Bundle savedInstanceState) {
+ createActionsInternal(actions);
+ }
- @Override
- public void onChannelBrowsableChanged() { }
- });
+ private void buildInputs() {
+ List<TvInputInfo> oldInputs = mInputs;
+ mInputs = mInputManager.getTvInputInfos(true, true);
+ // Get newly installed input ID.
+ if (oldInputs != null) {
+ List<TvInputInfo> newList = new ArrayList<>(mInputs);
+ for (TvInputInfo input : oldInputs) {
+ newList.remove(input);
+ }
+ if (newList.size() > 0 && mSetupUtils.isNewInput(newList.get(0).getId())) {
+ mNewlyAddedInputId = newList.get(0).getId();
+ } else {
+ mNewlyAddedInputId = null;
+ }
+ }
+ Collections.sort(mInputs, new TvInputNewComparator(mSetupUtils, mInputManager));
+ mKnownInputStartIndex = 0;
+ mDoneInputStartIndex = 0;
+ for (TvInputInfo input : mInputs) {
+ if (mSetupUtils.isNewInput(input.getId())) {
+ mSetupUtils.markAsKnownInput(input.getId());
+ ++mKnownInputStartIndex;
+ }
+ if (!mSetupUtils.isSetupDone(input.getId())) {
+ ++mDoneInputStartIndex;
+ }
}
}
@@ -147,39 +292,84 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
List<GuidedAction> actions = new ArrayList<>();
createActionsInternal(actions);
setActions(actions);
- mAdapter.notifyDataSetChanged();
}
private void createActionsInternal(List<GuidedAction> actions) {
- for (int i = 0; i < mInputList.size(); ++i) {
- if (mShowDivider && i == mKnownInputStartIndex) {
- actions.add(new GuidedAction.Builder().id(ACTION_DIVIDER).title(null)
- .description(null).build());
+ int newPosition = -1;
+ int position = 0;
+ if (mDoneInputStartIndex > 0) {
+ // Need a "New" category
+ actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_HEADER)
+ .title(null).description(getString(R.string.setup_category_new))
+ .focusable(false).build());
+ }
+ for (int i = 0; i < mInputs.size(); ++i) {
+ if (i == mDoneInputStartIndex) {
+ ++position;
+ actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_HEADER)
+ .title(null).description(getString(R.string.setup_category_done))
+ .focusable(false).build());
}
- TvInputInfo input = mInputList.get(i);
+ TvInputInfo input = mInputs.get(i);
+ String inputId = input.getId();
String description;
- int channelCount = mChannelDataManager.getChannelCountForInput(input.getId());
- if (mSetupUtils.isSetupDone(input.getId())) {
+ int channelCount = mChannelDataManager.getChannelCountForInput(inputId);
+ if (mSetupUtils.isSetupDone(inputId) || channelCount > 0) {
if (channelCount == 0) {
- description = getResources().getString(R.string.setup_input_no_channels);
+ description = getString(R.string.setup_input_no_channels);
} else {
description = getResources().getQuantityString(
R.plurals.setup_input_channels, channelCount, channelCount);
}
} else if (i >= mKnownInputStartIndex) {
- description = getResources().getString(R.string.channel_description_setup_now);
+ description = getString(R.string.setup_input_setup_now);
} else {
- description = getResources().getString(R.string.setup_input_new);
+ description = getString(R.string.setup_input_new);
}
- actions.add(new GuidedAction.Builder().id(ACTION_INPUT_START + i)
+ ++position;
+ if (input.getId().equals(mNewlyAddedInputId)) {
+ newPosition = position;
+ }
+ actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_INPUT_START + i)
.title(input.loadLabel(getActivity()).toString()).description(description)
.build());
}
+ if (Features.ONBOARDING_PLAY_STORE.isEnabled(getActivity())) {
+ if (mInputs.size() > 0) {
+ // Divider
+ ++position;
+ actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_DIVIDER)
+ .title(null).description(null).focusable(false).build());
+ }
+ // Play store action
+ ++position;
+ actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_PLAY_STORE)
+ .title(getString(R.string.setup_play_store_action_title))
+ .description(getString(R.string.setup_play_store_action_description))
+ .icon(R.drawable.ic_playstore).build());
+ }
+ if (newPosition != -1) {
+ VerticalGridView gridView = getGuidedActionsStylist().getActionsGridView();
+ gridView.setSelectedPosition(newPosition);
+ }
+ }
+
+ @Override
+ protected String getActionCategory() {
+ return ACTION_CATEGORY;
}
@Override
public void onGuidedActionClicked(GuidedAction action) {
- TvInputInfo input = mInputList.get((int) action.getId() - ACTION_INPUT_START);
+ if (action.getId() == ACTION_PLAY_STORE) {
+ mParentFragment.onActionClick(ACTION_CATEGORY, (int) action.getId());
+ return;
+ }
+ TvInputInfo input = mInputs.get((int) action.getId() - ACTION_INPUT_START);
+ if (mParentFragment.mInputSetupRunnable != null) {
+ mParentFragment.mInputSetupRunnable.runInputSetup(input);
+ return;
+ }
Intent intent = TvCommonUtils.createSetupIntent(input);
if (intent == null) {
Toast.makeText(getActivity(), R.string.msg_no_setup_activity, Toast.LENGTH_SHORT)
@@ -190,14 +380,13 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
// should go through Live channels SetupPassthroughActivity.
intent.setComponent(new ComponentName(getActivity(), SetupPassthroughActivity.class));
try {
- // Now we know that the user intends to set up this input. Grant permission for writing
- // EPG data.
+ // Now we know that the user intends to set up this input. Grant permission for
+ // writing EPG data.
SetupUtils.grantEpgPermission(getActivity(), input.getServiceInfo().packageName);
startActivityForResult(intent, REQUEST_CODE_START_SETUP_ACTIVITY);
} catch (ActivityNotFoundException e) {
Toast.makeText(getActivity(), getString(R.string.msg_unable_to_start_setup_activity,
input.loadLabel(getActivity())), Toast.LENGTH_SHORT).show();
- return;
}
}
@@ -206,67 +395,78 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
updateActions();
}
- @SuppressWarnings("rawtypes")
- private class SetupSourcesAdapter extends RecyclerView.Adapter {
- private static final int VIEW_TYPE_INPUT = 1;
- private static final int VIEW_TYPE_DIVIDER = 2;
-
- private final RecyclerView.Adapter mGuidedActionAdapter;
+ @Override
+ public int onProvideTheme() {
+ return sTheme == DEFAULT_THEME ? super.onProvideTheme() : sTheme;
+ }
- SetupSourcesAdapter(RecyclerView.Adapter adapter) {
- mGuidedActionAdapter = adapter;
+ void executePendingAction() {
+ switch (mPendingAction) {
+ case PENDING_ACTION_INPUT_CHANGED:
+ buildInputs();
+ // Fall through
+ case PENDING_ACTION_CHANNEL_CHANGED:
+ updateActions();
+ break;
}
+ mPendingAction = PENDING_ACTION_NONE;
+ }
+
+ private class SetupSourceGuidedActionsStylist extends GuidedActionsStylist {
+ private static final int VIEW_TYPE_DIVIDER = 1;
+
+ private static final float ALPHA_CATEGORY = 1.0f;
+ private static final float ALPHA_INPUT_DESCRIPTION = 0.5f;
@Override
- public int getItemViewType(int position) {
- if (mShowDivider && position == mKnownInputStartIndex) {
+ public int getItemViewType(GuidedAction action) {
+ if (action.getId() == ACTION_DIVIDER) {
return VIEW_TYPE_DIVIDER;
}
- return VIEW_TYPE_INPUT;
+ return super.getItemViewType(action);
}
@Override
- public int getItemCount() {
- if (mInputList == null) {
- return 0;
+ public int onProvideItemLayoutId(int viewType) {
+ if (viewType == VIEW_TYPE_DIVIDER) {
+ return R.layout.onboarding_item_divider;
}
- return mInputList.size() + (mShowDivider ? 1 : 0);
+ return super.onProvideItemLayoutId(viewType);
}
@Override
- public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- if (viewType == VIEW_TYPE_INPUT) {
- return mGuidedActionAdapter.onCreateViewHolder(parent, viewType);
+ public void onBindViewHolder(ViewHolder vh, GuidedAction action) {
+ super.onBindViewHolder(vh, action);
+ TextView descriptionView = vh.getDescriptionView();
+ if (descriptionView != null) {
+ if (action.getId() == ACTION_HEADER) {
+ descriptionView.setAlpha(ALPHA_CATEGORY);
+ descriptionView.setTextColor(Utils.getColor(getResources(),
+ R.color.setup_category));
+ descriptionView.setTypeface(Typeface.create(
+ getString(R.string.condensed_font), 0));
+ } else {
+ descriptionView.setAlpha(ALPHA_INPUT_DESCRIPTION);
+ descriptionView.setTextColor(Utils.getColor(getResources(),
+ R.color.common_setup_input_description));
+ descriptionView.setTypeface(Typeface.create(getString(R.string.font), 0));
+ }
}
- View itemView = LayoutInflater.from(parent.getContext()).inflate(
- R.layout.onboarding_item_divider, parent, false);
- return new MyViewHolder(itemView);
- }
-
- @SuppressWarnings("unchecked")
- @Override
- public void onBindViewHolder(ViewHolder viewHolder, int position) {
- if (mShowDivider && position == mKnownInputStartIndex) {
- return;
+ // Workaround for b/26473407.
+ ImageView iconView = vh.getIconView();
+ if (iconView != null) {
+ Drawable icon = action.getIcon();
+ if (icon != null) {
+ // setImageDrawable resets the drawable's level unless we set the view level
+ // first.
+ iconView.setImageLevel(icon.getLevel());
+ iconView.setImageDrawable(icon);
+ iconView.setVisibility(View.VISIBLE);
+ } else {
+ iconView.setVisibility(View.GONE);
+ }
}
- mGuidedActionAdapter.onBindViewHolder(viewHolder, position);
- }
-
- @Override
- public void onAttachedToRecyclerView(RecyclerView recyclerView) {
- mGuidedActionAdapter.onAttachedToRecyclerView(recyclerView);
- }
-
- @Override
- public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
- mGuidedActionAdapter.onDetachedFromRecyclerView(recyclerView);
}
}
}
-
- private static class MyViewHolder extends RecyclerView.ViewHolder {
- public MyViewHolder(View itemView) {
- super(itemView);
- }
- }
}
diff --git a/src/com/android/tv/onboarding/WelcomeFragment.java b/src/com/android/tv/onboarding/WelcomeFragment.java
index f8cd8ee7..ed85df68 100644
--- a/src/com/android/tv/onboarding/WelcomeFragment.java
+++ b/src/com/android/tv/onboarding/WelcomeFragment.java
@@ -18,41 +18,41 @@ package com.android.tv.onboarding;
import android.animation.Animator;
import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.os.Bundle;
-import android.transition.TransitionValues;
+import android.support.annotation.Nullable;
+import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
-import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.android.tv.R;
-import com.android.tv.common.ui.setup.SetupFragment;
-import com.android.tv.common.ui.setup.animation.CustomTransition;
-import com.android.tv.common.ui.setup.animation.CustomTransitionProvider;
+import com.android.tv.common.ui.setup.SetupActionHelper;
import com.android.tv.common.ui.setup.animation.SetupAnimationHelper;
+import com.android.tv.common.ui.setup.leanback.OnboardingFragment;
+
+import java.util.ArrayList;
+import java.util.List;
/**
- * A fragment for the onboarding screen.
+ * A fragment for the onboarding welcome screen.
*/
-public class WelcomeFragment extends SetupFragment {
+public class WelcomeFragment extends OnboardingFragment {
+ public static final String ACTION_CATEGORY = "comgoogle.android.tv.onboarding.WelcomeFragment";
public static final int ACTION_NEXT = 1;
- private static final long LOGO_SPLASH_PAUSE_DURATION_MS = 333;
- private static final long LOGO_SPLASH_DURATION_MS = 1000;
- private static final long START_DELAY_PAGE_INDICATOR_MS = LOGO_SPLASH_DURATION_MS;
- private static final long START_DELAY_TITLE_MS = LOGO_SPLASH_DURATION_MS + 33;
- private static final long START_DELAY_DESCRIPTION_MS = LOGO_SPLASH_DURATION_MS + 33;
- private static final long START_DELAY_CLOUD_MS = LOGO_SPLASH_DURATION_MS + 33;
- private static final long START_DELAY_TV_MS = LOGO_SPLASH_DURATION_MS + 567;
- private static final long START_DELAY_TV_CONTENTS_MS = 266;
- private static final long START_DELAY_SHADOW_MS = LOGO_SPLASH_DURATION_MS + 567;
+ private static final long START_DELAY_CLOUD_MS = 33;
+ private static final long START_DELAY_TV_MS = 567;
+ private static final long START_DELAY_TV_CONTENTS_MS = 833;
+ private static final long START_DELAY_SHADOW_MS = 567;
- private static final long WELCOME_PAGE_TRANSITION_DURATION_MS = 417;
+ private static final long VIDEO_FADE_OUT_DURATION_MS = 333;
private static final long BLUE_SCREEN_HOLD_DURATION_MS = 1500;
+ // TODO: Use animator list xml.
private static final int[] TV_FRAMES_1_START = {
R.drawable.tv_1a_01,
R.drawable.tv_1a_02,
@@ -73,8 +73,7 @@ public class WelcomeFragment extends SetupFragment {
R.drawable.tv_1a_17,
R.drawable.tv_1a_18,
R.drawable.tv_1a_19,
- R.drawable.tv_1a_20,
- 0
+ R.drawable.tv_1a_20
};
private static final int[] TV_FRAMES_1_END = {
@@ -88,11 +87,238 @@ public class WelcomeFragment extends SetupFragment {
R.drawable.tv_1b_08,
R.drawable.tv_1b_09,
R.drawable.tv_1b_10,
- R.drawable.tv_1b_11,
- 0
+ R.drawable.tv_1b_11
+ };
+
+ private static final int[] TV_FRAMES_2_START = {
+ R.drawable.tv_5a_0,
+ R.drawable.tv_5a_1,
+ R.drawable.tv_5a_2,
+ R.drawable.tv_5a_3,
+ R.drawable.tv_5a_4,
+ R.drawable.tv_5a_5,
+ R.drawable.tv_5a_6,
+ R.drawable.tv_5a_7,
+ R.drawable.tv_5a_8,
+ R.drawable.tv_5a_9,
+ R.drawable.tv_5a_10,
+ R.drawable.tv_5a_11,
+ R.drawable.tv_5a_12,
+ R.drawable.tv_5a_13,
+ R.drawable.tv_5a_14,
+ R.drawable.tv_5a_15,
+ R.drawable.tv_5a_16,
+ R.drawable.tv_5a_17,
+ R.drawable.tv_5a_18,
+ R.drawable.tv_5a_19,
+ R.drawable.tv_5a_20,
+ R.drawable.tv_5a_21,
+ R.drawable.tv_5a_22,
+ R.drawable.tv_5a_23,
+ R.drawable.tv_5a_24,
+ R.drawable.tv_5a_25,
+ R.drawable.tv_5a_26,
+ R.drawable.tv_5a_27,
+ R.drawable.tv_5a_28,
+ R.drawable.tv_5a_29,
+ R.drawable.tv_5a_30,
+ R.drawable.tv_5a_31,
+ R.drawable.tv_5a_32,
+ R.drawable.tv_5a_33,
+ R.drawable.tv_5a_34,
+ R.drawable.tv_5a_35,
+ R.drawable.tv_5a_36,
+ R.drawable.tv_5a_37,
+ R.drawable.tv_5a_38,
+ R.drawable.tv_5a_39,
+ R.drawable.tv_5a_40,
+ R.drawable.tv_5a_41,
+ R.drawable.tv_5a_42,
+ R.drawable.tv_5a_43,
+ R.drawable.tv_5a_44,
+ R.drawable.tv_5a_45,
+ R.drawable.tv_5a_46,
+ R.drawable.tv_5a_47,
+ R.drawable.tv_5a_48,
+ R.drawable.tv_5a_49,
+ R.drawable.tv_5a_50,
+ R.drawable.tv_5a_51,
+ R.drawable.tv_5a_52,
+ R.drawable.tv_5a_53,
+ R.drawable.tv_5a_54,
+ R.drawable.tv_5a_55,
+ R.drawable.tv_5a_56,
+ R.drawable.tv_5a_57,
+ R.drawable.tv_5a_58,
+ R.drawable.tv_5a_59,
+ R.drawable.tv_5a_60,
+ R.drawable.tv_5a_61,
+ R.drawable.tv_5a_62,
+ R.drawable.tv_5a_63,
+ R.drawable.tv_5a_64,
+ R.drawable.tv_5a_65,
+ R.drawable.tv_5a_66,
+ R.drawable.tv_5a_67,
+ R.drawable.tv_5a_68,
+ R.drawable.tv_5a_69,
+ R.drawable.tv_5a_70,
+ R.drawable.tv_5a_71,
+ R.drawable.tv_5a_72,
+ R.drawable.tv_5a_73,
+ R.drawable.tv_5a_74,
+ R.drawable.tv_5a_75,
+ R.drawable.tv_5a_76,
+ R.drawable.tv_5a_77,
+ R.drawable.tv_5a_78,
+ R.drawable.tv_5a_79,
+ R.drawable.tv_5a_80,
+ R.drawable.tv_5a_81,
+ R.drawable.tv_5a_82,
+ R.drawable.tv_5a_83,
+ R.drawable.tv_5a_84,
+ R.drawable.tv_5a_85,
+ R.drawable.tv_5a_86,
+ R.drawable.tv_5a_87,
+ R.drawable.tv_5a_88,
+ R.drawable.tv_5a_89,
+ R.drawable.tv_5a_90,
+ R.drawable.tv_5a_91,
+ R.drawable.tv_5a_92,
+ R.drawable.tv_5a_93,
+ R.drawable.tv_5a_94,
+ R.drawable.tv_5a_95,
+ R.drawable.tv_5a_96,
+ R.drawable.tv_5a_97,
+ R.drawable.tv_5a_98,
+ R.drawable.tv_5a_99,
+ R.drawable.tv_5a_100,
+ R.drawable.tv_5a_101,
+ R.drawable.tv_5a_102,
+ R.drawable.tv_5a_103,
+ R.drawable.tv_5a_104,
+ R.drawable.tv_5a_105,
+ R.drawable.tv_5a_106,
+ R.drawable.tv_5a_107,
+ R.drawable.tv_5a_108,
+ R.drawable.tv_5a_109,
+ R.drawable.tv_5a_110,
+ R.drawable.tv_5a_111,
+ R.drawable.tv_5a_112,
+ R.drawable.tv_5a_113,
+ R.drawable.tv_5a_114,
+ R.drawable.tv_5a_115,
+ R.drawable.tv_5a_116,
+ R.drawable.tv_5a_117,
+ R.drawable.tv_5a_118,
+ R.drawable.tv_5a_119,
+ R.drawable.tv_5a_120,
+ R.drawable.tv_5a_121,
+ R.drawable.tv_5a_122,
+ R.drawable.tv_5a_123,
+ R.drawable.tv_5a_124,
+ R.drawable.tv_5a_125,
+ R.drawable.tv_5a_126,
+ R.drawable.tv_5a_127,
+ R.drawable.tv_5a_128,
+ R.drawable.tv_5a_129,
+ R.drawable.tv_5a_130,
+ R.drawable.tv_5a_131,
+ R.drawable.tv_5a_132,
+ R.drawable.tv_5a_133,
+ R.drawable.tv_5a_134,
+ R.drawable.tv_5a_135,
+ R.drawable.tv_5a_136,
+ R.drawable.tv_5a_137,
+ R.drawable.tv_5a_138,
+ R.drawable.tv_5a_139,
+ R.drawable.tv_5a_140,
+ R.drawable.tv_5a_141,
+ R.drawable.tv_5a_142,
+ R.drawable.tv_5a_143,
+ R.drawable.tv_5a_144,
+ R.drawable.tv_5a_145,
+ R.drawable.tv_5a_146,
+ R.drawable.tv_5a_147,
+ R.drawable.tv_5a_148,
+ R.drawable.tv_5a_149,
+ R.drawable.tv_5a_150,
+ R.drawable.tv_5a_151,
+ R.drawable.tv_5a_152,
+ R.drawable.tv_5a_153,
+ R.drawable.tv_5a_154,
+ R.drawable.tv_5a_155,
+ R.drawable.tv_5a_156,
+ R.drawable.tv_5a_157,
+ R.drawable.tv_5a_158,
+ R.drawable.tv_5a_159,
+ R.drawable.tv_5a_160,
+ R.drawable.tv_5a_161,
+ R.drawable.tv_5a_162,
+ R.drawable.tv_5a_163,
+ R.drawable.tv_5a_164,
+ R.drawable.tv_5a_165,
+ R.drawable.tv_5a_166,
+ R.drawable.tv_5a_167,
+ R.drawable.tv_5a_168,
+ R.drawable.tv_5a_169,
+ R.drawable.tv_5a_170,
+ R.drawable.tv_5a_171,
+ R.drawable.tv_5a_172,
+ R.drawable.tv_5a_173,
+ R.drawable.tv_5a_174,
+ R.drawable.tv_5a_175,
+ R.drawable.tv_5a_176,
+ R.drawable.tv_5a_177,
+ R.drawable.tv_5a_178,
+ R.drawable.tv_5a_179,
+ R.drawable.tv_5a_180,
+ R.drawable.tv_5a_181,
+ R.drawable.tv_5a_182,
+ R.drawable.tv_5a_183,
+ R.drawable.tv_5a_184,
+ R.drawable.tv_5a_185,
+ R.drawable.tv_5a_186,
+ R.drawable.tv_5a_187,
+ R.drawable.tv_5a_188,
+ R.drawable.tv_5a_189,
+ R.drawable.tv_5a_190,
+ R.drawable.tv_5a_191,
+ R.drawable.tv_5a_192,
+ R.drawable.tv_5a_193,
+ R.drawable.tv_5a_194,
+ R.drawable.tv_5a_195,
+ R.drawable.tv_5a_196,
+ R.drawable.tv_5a_197,
+ R.drawable.tv_5a_198,
+ R.drawable.tv_5a_199,
+ R.drawable.tv_5a_200,
+ R.drawable.tv_5a_201,
+ R.drawable.tv_5a_202,
+ R.drawable.tv_5a_203,
+ R.drawable.tv_5a_204,
+ R.drawable.tv_5a_205,
+ R.drawable.tv_5a_206,
+ R.drawable.tv_5a_207,
+ R.drawable.tv_5a_208,
+ R.drawable.tv_5a_209,
+ R.drawable.tv_5a_210,
+ R.drawable.tv_5a_211,
+ R.drawable.tv_5a_212,
+ R.drawable.tv_5a_213,
+ R.drawable.tv_5a_214,
+ R.drawable.tv_5a_215,
+ R.drawable.tv_5a_216,
+ R.drawable.tv_5a_217,
+ R.drawable.tv_5a_218,
+ R.drawable.tv_5a_219,
+ R.drawable.tv_5a_220,
+ R.drawable.tv_5a_221,
+ R.drawable.tv_5a_222,
+ R.drawable.tv_5a_223,
+ R.drawable.tv_5a_224
};
- private static final int[] TV_FRAMES_2_BLUE_ARROW = {
+ private static final int[] TV_FRAMES_3_BLUE_ARROW = {
R.drawable.arrow_blue_00,
R.drawable.arrow_blue_01,
R.drawable.arrow_blue_02,
@@ -153,11 +379,10 @@ public class WelcomeFragment extends SetupFragment {
R.drawable.arrow_blue_57,
R.drawable.arrow_blue_58,
R.drawable.arrow_blue_59,
- R.drawable.arrow_blue_60,
- 0
+ R.drawable.arrow_blue_60
};
- private static final int[] TV_FRAMES_2_BLUE_START = {
+ private static final int[] TV_FRAMES_3_BLUE_START = {
R.drawable.tv_2a_01,
R.drawable.tv_2a_02,
R.drawable.tv_2a_03,
@@ -176,11 +401,10 @@ public class WelcomeFragment extends SetupFragment {
R.drawable.tv_2a_16,
R.drawable.tv_2a_17,
R.drawable.tv_2a_18,
- R.drawable.tv_2a_19,
- 0
+ R.drawable.tv_2a_19
};
- private static final int[] TV_FRAMES_2_BLUE_END = {
+ private static final int[] TV_FRAMES_3_BLUE_END = {
R.drawable.tv_2b_01,
R.drawable.tv_2b_02,
R.drawable.tv_2b_03,
@@ -199,11 +423,10 @@ public class WelcomeFragment extends SetupFragment {
R.drawable.tv_2b_16,
R.drawable.tv_2b_17,
R.drawable.tv_2b_18,
- R.drawable.tv_2b_19,
- 0
+ R.drawable.tv_2b_19
};
- private static final int[] TV_FRAMES_2_ORANGE_ARROW = {
+ private static final int[] TV_FRAMES_3_ORANGE_ARROW = {
R.drawable.arrow_orange_180,
R.drawable.arrow_orange_181,
R.drawable.arrow_orange_182,
@@ -264,11 +487,10 @@ public class WelcomeFragment extends SetupFragment {
R.drawable.arrow_orange_237,
R.drawable.arrow_orange_238,
R.drawable.arrow_orange_239,
- R.drawable.arrow_orange_240,
- 0
+ R.drawable.arrow_orange_240
};
- private static final int[] TV_FRAMES_2_ORANGE_START = {
+ private static final int[] TV_FRAMES_3_ORANGE_START = {
R.drawable.tv_2c_01,
R.drawable.tv_2c_02,
R.drawable.tv_2c_03,
@@ -284,11 +506,10 @@ public class WelcomeFragment extends SetupFragment {
R.drawable.tv_2c_13,
R.drawable.tv_2c_14,
R.drawable.tv_2c_15,
- R.drawable.tv_2c_16,
- 0
+ R.drawable.tv_2c_16
};
- private static final int[] TV_FRAMES_3_START = {
+ private static final int[] TV_FRAMES_4_START = {
R.drawable.tv_3a_01,
R.drawable.tv_3a_02,
R.drawable.tv_3a_03,
@@ -349,439 +570,198 @@ public class WelcomeFragment extends SetupFragment {
R.drawable.tv_3b_115,
R.drawable.tv_3b_116,
R.drawable.tv_3b_117,
- R.drawable.tv_3b_118,
- 0
- };
-
- private static final int[] TV_FRAMES_4_START = {
- R.drawable.tv_4a_15,
- R.drawable.tv_4a_16,
- R.drawable.tv_4a_17,
- R.drawable.tv_4a_18,
- R.drawable.tv_4a_19,
- R.drawable.tv_4a_20,
- R.drawable.tv_4a_21,
- R.drawable.tv_4a_22,
- R.drawable.tv_4a_23,
- R.drawable.tv_4a_24,
- R.drawable.tv_4a_25,
- R.drawable.tv_4a_26,
- R.drawable.tv_4a_27,
- R.drawable.tv_4a_28,
- R.drawable.tv_4a_29,
- R.drawable.tv_4a_30,
- R.drawable.tv_4a_31,
- R.drawable.tv_4a_32,
- R.drawable.tv_4a_33,
- R.drawable.tv_4a_34,
- R.drawable.tv_4a_35,
- R.drawable.tv_4a_36,
- R.drawable.tv_4a_37,
- R.drawable.tv_4a_38,
- R.drawable.tv_4a_39,
- R.drawable.tv_4a_40,
- R.drawable.tv_4a_41,
- R.drawable.tv_4a_42,
- R.drawable.tv_4a_43,
- R.drawable.tv_4a_44,
- R.drawable.tv_4a_45,
- R.drawable.tv_4a_46,
- R.drawable.tv_4a_47,
- R.drawable.tv_4a_48,
- R.drawable.tv_4a_49,
- R.drawable.tv_4a_50,
- R.drawable.tv_4a_51,
- R.drawable.tv_4a_52,
- R.drawable.tv_4a_53,
- R.drawable.tv_4a_54,
- R.drawable.tv_4a_55,
- R.drawable.tv_4a_56,
- R.drawable.tv_4a_57,
- R.drawable.tv_4a_58,
- R.drawable.tv_4a_59,
- R.drawable.tv_4a_60,
- R.drawable.tv_4a_61,
- R.drawable.tv_4a_62,
- R.drawable.tv_4a_63,
- R.drawable.tv_4a_64,
- R.drawable.tv_4a_65,
- R.drawable.tv_4a_66,
- R.drawable.tv_4a_67,
- R.drawable.tv_4a_68,
- R.drawable.tv_4a_69,
- R.drawable.tv_4a_70,
- R.drawable.tv_4a_71,
- R.drawable.tv_4a_72,
- R.drawable.tv_4a_73,
- R.drawable.tv_4a_74,
- R.drawable.tv_4a_75,
- R.drawable.tv_4a_76,
- R.drawable.tv_4a_77,
- R.drawable.tv_4a_78,
- R.drawable.tv_4a_79,
- R.drawable.tv_4a_80,
- R.drawable.tv_4a_81,
- R.drawable.tv_4a_82,
- R.drawable.tv_4a_83,
- R.drawable.tv_4a_84,
- R.drawable.tv_4a_85,
- R.drawable.tv_4a_86,
- R.drawable.tv_4a_87,
- R.drawable.tv_4a_88,
- R.drawable.tv_4a_89,
- R.drawable.tv_4a_90,
- R.drawable.tv_4a_91,
- R.drawable.tv_4a_92,
- R.drawable.tv_4a_93,
- R.drawable.tv_4a_94,
- R.drawable.tv_4a_95,
- R.drawable.tv_4a_96,
- R.drawable.tv_4a_97,
- R.drawable.tv_4a_98,
- R.drawable.tv_4a_99,
- R.drawable.tv_4a_100,
- R.drawable.tv_4a_101,
- R.drawable.tv_4a_102,
- R.drawable.tv_4a_103,
- R.drawable.tv_4a_104,
- R.drawable.tv_4a_105,
- R.drawable.tv_4a_106,
- R.drawable.tv_4a_107,
- R.drawable.tv_4a_108,
- R.drawable.tv_4a_109,
- R.drawable.tv_4a_110,
- R.drawable.tv_4a_111,
- R.drawable.tv_4a_112,
- R.drawable.tv_4a_113,
- R.drawable.tv_4a_114,
- R.drawable.tv_4a_115,
- R.drawable.tv_4a_116,
- R.drawable.tv_4a_117,
- R.drawable.tv_4a_118,
- R.drawable.tv_4a_119,
- R.drawable.tv_4a_120,
- R.drawable.tv_4a_121,
- R.drawable.tv_4a_122,
- R.drawable.tv_4a_123,
- R.drawable.tv_4a_124,
- R.drawable.tv_4a_125,
- R.drawable.tv_4a_126,
- R.drawable.tv_4a_127,
- R.drawable.tv_4a_128,
- R.drawable.tv_4a_129,
- R.drawable.tv_4a_130,
- R.drawable.tv_4a_131,
- R.drawable.tv_4a_132,
- R.drawable.tv_4a_133,
- R.drawable.tv_4a_134,
- R.drawable.tv_4a_135,
- R.drawable.tv_4a_136,
- R.drawable.tv_4a_137,
- R.drawable.tv_4a_138,
- R.drawable.tv_4a_139,
- R.drawable.tv_4a_140,
- R.drawable.tv_4a_141,
- R.drawable.tv_4a_142,
- R.drawable.tv_4a_143,
- R.drawable.tv_4a_144,
- R.drawable.tv_4a_145,
- R.drawable.tv_4a_146,
- R.drawable.tv_4a_147,
- R.drawable.tv_4a_148,
- R.drawable.tv_4a_149,
- R.drawable.tv_4a_150,
- R.drawable.tv_4a_151,
- R.drawable.tv_4a_152,
- R.drawable.tv_4a_153,
- R.drawable.tv_4a_154,
- R.drawable.tv_4a_155,
- R.drawable.tv_4a_156,
- R.drawable.tv_4a_157,
- R.drawable.tv_4a_158,
- R.drawable.tv_4a_159,
- R.drawable.tv_4a_160,
- R.drawable.tv_4a_161,
- R.drawable.tv_4a_162,
- R.drawable.tv_4a_163,
- R.drawable.tv_4a_164,
- R.drawable.tv_4a_165,
- R.drawable.tv_4a_166,
- R.drawable.tv_4a_167,
- R.drawable.tv_4a_168,
- R.drawable.tv_4a_169,
- R.drawable.tv_4a_170,
- R.drawable.tv_4a_171,
- R.drawable.tv_4a_172,
- R.drawable.tv_4a_173,
- R.drawable.tv_4a_174,
- R.drawable.tv_4a_175,
- R.drawable.tv_4a_176,
- R.drawable.tv_4a_177,
- R.drawable.tv_4a_178,
- R.drawable.tv_4a_179,
- R.drawable.tv_4a_180,
- R.drawable.tv_4a_181,
- R.drawable.tv_4a_182,
- R.drawable.tv_4a_183,
- R.drawable.tv_4a_184,
- R.drawable.tv_4a_185,
- R.drawable.tv_4a_186,
- R.drawable.tv_4a_187,
- R.drawable.tv_4a_188,
- R.drawable.tv_4a_189,
- R.drawable.tv_4a_190,
- R.drawable.tv_4a_191,
- R.drawable.tv_4a_192,
- R.drawable.tv_4a_193,
- R.drawable.tv_4a_194,
- R.drawable.tv_4a_195,
- R.drawable.tv_4a_196,
- R.drawable.tv_4a_197,
- R.drawable.tv_4a_198,
- R.drawable.tv_4a_199,
- R.drawable.tv_4a_200,
- R.drawable.tv_4a_201,
- R.drawable.tv_4a_202,
- R.drawable.tv_4a_203,
- R.drawable.tv_4a_204,
- R.drawable.tv_4a_205,
- R.drawable.tv_4a_206,
- R.drawable.tv_4a_207,
- R.drawable.tv_4a_208,
- R.drawable.tv_4a_209,
- R.drawable.tv_4a_210,
- R.drawable.tv_4a_211,
- R.drawable.tv_4a_212,
- R.drawable.tv_4a_213,
- R.drawable.tv_4a_214,
- R.drawable.tv_4a_215,
- R.drawable.tv_4a_216,
- R.drawable.tv_4a_217,
- R.drawable.tv_4a_218,
- R.drawable.tv_4a_219,
- R.drawable.tv_4a_220,
- R.drawable.tv_4a_221,
- R.drawable.tv_4a_222,
- R.drawable.tv_4a_223,
- R.drawable.tv_4a_224,
- R.drawable.tv_4a_225,
- R.drawable.tv_4a_226,
- R.drawable.tv_4a_227,
- R.drawable.tv_4a_228,
- R.drawable.tv_4a_229,
- R.drawable.tv_4a_230,
- R.drawable.tv_4a_231,
- R.drawable.tv_4a_232,
- R.drawable.tv_4a_233,
- R.drawable.tv_4a_234,
- R.drawable.tv_4a_235,
- R.drawable.tv_4a_236,
- R.drawable.tv_4a_237,
- R.drawable.tv_4a_238,
- R.drawable.tv_4a_239,
- 0
+ R.drawable.tv_3b_118
};
- private int mNumPages;
private String[] mPageTitles;
private String[] mPageDescriptions;
- private int mCurrentPageIndex;
- private int mPageTransitionDistance;
private ImageView mTvContentView;
- private PagingIndicator mPageIndicator;
private ImageView mArrowView;
- private View mLogoView;
private Animator mAnimator;
+ private boolean mNeedToEndAnimator;
public WelcomeFragment() {
- enableFragmentTransition(FRAGMENT_EXIT_TRANSITION);
- setEnterTransition(new CustomTransition(new CustomTransitionProvider() {
- @Override
- public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
- TransitionValues endValues) {
- Animator animator = null;
- switch (endValues.view.getId()) {
- case R.id.logo: {
- Animator inAnimator = AnimatorInflater.loadAnimator(getActivity(),
- R.animator.onboarding_welcome_logo_enter);
- Animator outAnimator = AnimatorInflater.loadAnimator(getActivity(),
- R.animator.onboarding_welcome_logo_exit);
- outAnimator.setStartDelay(LOGO_SPLASH_PAUSE_DURATION_MS);
- animator = new AnimatorSet();
- ((AnimatorSet) animator).playSequentially(inAnimator, outAnimator);
- animator.setTarget(view);
- break;
- }
- case R.id.page_indicator:
- view.setAlpha(0);
- animator = AnimatorInflater.loadAnimator(getActivity(),
- R.animator.onboarding_welcome_page_indicator_enter);
- animator.setStartDelay(START_DELAY_PAGE_INDICATOR_MS);
- animator.setTarget(view);
- break;
- case R.id.title:
- view.setAlpha(0);
- animator = AnimatorInflater.loadAnimator(getActivity(),
- R.animator.onboarding_welcome_title_enter);
- animator.setStartDelay(START_DELAY_TITLE_MS);
- animator.setTarget(view);
- break;
- case R.id.description:
- view.setAlpha(0);
- animator = AnimatorInflater.loadAnimator(getActivity(),
- R.animator.onboarding_welcome_description_enter);
- animator.setStartDelay(START_DELAY_DESCRIPTION_MS);
- animator.setTarget(view);
- break;
- case R.id.cloud1:
- case R.id.cloud2:
- view.setAlpha(0);
- animator = AnimatorInflater.loadAnimator(getActivity(),
- R.animator.onboarding_welcome_cloud_enter);
- animator.setStartDelay(START_DELAY_CLOUD_MS);
- animator.setTarget(view);
- break;
- case R.id.tv_container: {
- view.setAlpha(0);
- Animator tvAnimator = AnimatorInflater.loadAnimator(getActivity(),
- R.animator.onboarding_welcome_tv_enter);
- tvAnimator.setTarget(view);
- Animator frameAnimator = SetupAnimationHelper.createFrameAnimator(
- mTvContentView, TV_FRAMES_1_START);
- frameAnimator.setStartDelay(START_DELAY_TV_CONTENTS_MS);
- frameAnimator.setTarget(mTvContentView);
- animator = new AnimatorSet();
- ((AnimatorSet) animator).playTogether(tvAnimator, frameAnimator);
- animator.setStartDelay(START_DELAY_TV_MS);
- break;
- }
- case R.id.shadow:
- view.setAlpha(0);
- animator = AnimatorInflater.loadAnimator(getActivity(),
- R.animator.onboarding_welcome_shadow_enter);
- animator.setStartDelay(START_DELAY_SHADOW_MS);
- animator.setTarget(view);
- break;
- }
- return animator;
- }
-
- @Override
- public Animator onDisappear(ViewGroup sceneRoot, View view,
- TransitionValues startValues, TransitionValues endValues) {
- return null;
- }
- }));
+ setExitTransition(new SetupAnimationHelper.TransitionBuilder()
+ .setSlideEdge(Gravity.START)
+ .setParentIdsForDelay(new int[]{R.id.onboarding_fragment_root})
+ .build());
}
+ @Nullable
@Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View view = super.onCreateView(inflater, container, savedInstanceState);
- mAnimator = null;
- mPageTransitionDistance = getResources().getDimensionPixelOffset(
- R.dimen.onboarding_welcome_page_transition_distance);
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mPageTitles = getResources().getStringArray(R.array.welcome_page_titles);
mPageDescriptions = getResources().getStringArray(R.array.welcome_page_descriptions);
- mNumPages = mPageTitles.length;
- mCurrentPageIndex = 0;
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+
+ @Override
+ protected void onStartEnterAnimation() {
+ List<Animator> animators = new ArrayList<>();
+ // Cloud 1
+ View view = getActivity().findViewById(R.id.cloud1);
+ view.setAlpha(0);
+ Animator animator = AnimatorInflater.loadAnimator(getActivity(),
+ R.animator.onboarding_welcome_cloud_enter);
+ animator.setStartDelay(START_DELAY_CLOUD_MS);
+ animator.setTarget(view);
+ animators.add(animator);
+ // Cloud 2
+ view = getActivity().findViewById(R.id.cloud2);
+ view.setAlpha(0);
+ animator = AnimatorInflater.loadAnimator(getActivity(),
+ R.animator.onboarding_welcome_cloud_enter);
+ animator.setStartDelay(START_DELAY_CLOUD_MS);
+ animator.setTarget(view);
+ animators.add(animator);
+ // TV container
+ view = getActivity().findViewById(R.id.tv_container);
+ view.setAlpha(0);
+ animator = AnimatorInflater.loadAnimator(getActivity(),
+ R.animator.onboarding_welcome_tv_enter);
+ animator.setStartDelay(START_DELAY_TV_MS);
+ animator.setTarget(view);
+ animators.add(animator);
+ // TV content
+ view = getActivity().findViewById(R.id.tv_content);
+ animator = SetupAnimationHelper.createFrameAnimator((ImageView) view, TV_FRAMES_1_START);
+ animator.setStartDelay(START_DELAY_TV_CONTENTS_MS);
+ animator.setTarget(view);
+ animators.add(animator);
+ // Shadow
+ view = getActivity().findViewById(R.id.shadow);
+ view.setAlpha(0);
+ animator = AnimatorInflater.loadAnimator(getActivity(),
+ R.animator.onboarding_welcome_shadow_enter);
+ animator.setStartDelay(START_DELAY_SHADOW_MS);
+ animator.setTarget(view);
+ animators.add(animator);
+ AnimatorSet set = new AnimatorSet();
+ set.playTogether(animators);
+ mAnimator = set;
+ mAnimator.start();
+ mNeedToEndAnimator = true;
+ }
+
+ @Nullable
+ @Override
+ protected View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container) {
+ return inflater.inflate(R.layout.onboarding_welcome_background, container, false);
+ }
+
+ @Nullable
+ @Override
+ protected View onCreateContentView(LayoutInflater inflater, ViewGroup container) {
+ View view = inflater.inflate(R.layout.onboarding_welcome_content, container, false);
mTvContentView = (ImageView) view.findViewById(R.id.tv_content);
- mPageIndicator = (PagingIndicator) view.findViewById(R.id.page_indicator);
- mPageIndicator.setPageCount(mNumPages);
- mPageIndicator.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View view) {
- if (mCurrentPageIndex == mNumPages - 1) {
- onActionClick(ACTION_NEXT);
- } else {
- showPage(++mCurrentPageIndex);
- startTvFrameAnimation(mCurrentPageIndex);
- }
- }
- });
- mArrowView = (ImageView) view.findViewById(R.id.arrow);
- mLogoView = view.findViewById(R.id.logo);
- showPage(mCurrentPageIndex);
return view;
}
+ @Nullable
@Override
- protected int getLayoutResourceId() {
- return R.layout.fragment_welcome;
+ protected View onCreateForegroundView(LayoutInflater inflater, ViewGroup container) {
+ mArrowView = (ImageView) inflater.inflate(R.layout.onboarding_welcome_foreground, container,
+ false);
+ return mArrowView;
}
- /*
- * 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;
+ @Override
+ protected int getPageCount() {
+ return mPageTitles.length;
}
- private void showPage(final int pageIndex) {
- SetupFragment fragment = getPage(pageIndex);
- if (pageIndex == 0) {
- fragment.enableFragmentTransition(FRAGMENT_EXIT_TRANSITION);
- }
- if (pageIndex == mNumPages - 1) {
- fragment.enableFragmentTransition(FRAGMENT_ENTER_TRANSITION);
- }
- fragment.setTransitionDistance(mPageTransitionDistance);
- fragment.setTransitionDuration(WELCOME_PAGE_TRANSITION_DURATION_MS);
- getChildFragmentManager().beginTransaction().replace(R.id.page_container, fragment)
- .commit();
- mPageIndicator.onPageSelected(pageIndex, pageIndex != 0);
+ @Override
+ protected String getPageTitle(int pageIndex) {
+ return mPageTitles[pageIndex];
+ }
+
+ @Override
+ protected String getPageDescription(int pageIndex) {
+ return mPageDescriptions[pageIndex];
}
@Override
- protected int[] getParentIdsForDelay() {
- return new int[] {R.id.welcome_fragment_root};
+ protected int getLogoResourceId() {
+ return R.drawable.splash_logo;
}
- private void startTvFrameAnimation(int newPageIndex) {
+ @Override
+ protected void onFinishFragment() {
+ SetupActionHelper.onActionClick(WelcomeFragment.this, ACTION_CATEGORY, ACTION_NEXT);
+ }
+
+ @Override
+ protected void onStartPageChangeAnimation(int previousPage) {
if (mAnimator != null) {
- mAnimator.cancel();
+ if (mNeedToEndAnimator) {
+ mAnimator.end();
+ } else {
+ mAnimator.cancel();
+ }
}
- // TODO: Change the magic numbers to constants once the animation specification is given.
+ mArrowView.setVisibility(View.GONE);
+ // TV screen hiding animator.
+ Animator hideAnimator = previousPage == 0
+ ? SetupAnimationHelper.createFrameAnimator(mTvContentView, TV_FRAMES_1_END)
+ : SetupAnimationHelper.createFadeOutAnimator(mTvContentView,
+ VIDEO_FADE_OUT_DURATION_MS, true);
+ // TV screen showing animator.
AnimatorSet animatorSet = new AnimatorSet();
- switch (newPageIndex) {
+ int firstFrame;
+ switch (getCurrentPageIndex()) {
+ case 0:
+ animatorSet.playSequentially(hideAnimator,
+ SetupAnimationHelper.createFrameAnimator(mTvContentView,
+ TV_FRAMES_1_START));
+ firstFrame = TV_FRAMES_1_START[0];
+ break;
case 1:
- mLogoView.setVisibility(View.GONE);
- animatorSet.playSequentially(
- SetupAnimationHelper.createFrameAnimator(mTvContentView, TV_FRAMES_1_END),
+ animatorSet.playSequentially(hideAnimator,
+ SetupAnimationHelper.createFrameAnimator(mTvContentView,
+ TV_FRAMES_2_START));
+ firstFrame = TV_FRAMES_2_START[0];
+ break;
+ case 2:
+ mArrowView.setVisibility(View.VISIBLE);
+ animatorSet.playSequentially(hideAnimator,
SetupAnimationHelper.createFrameAnimator(mArrowView,
- TV_FRAMES_2_BLUE_ARROW),
+ TV_FRAMES_3_BLUE_ARROW),
SetupAnimationHelper.createFrameAnimator(mTvContentView,
- TV_FRAMES_2_BLUE_START),
+ TV_FRAMES_3_BLUE_START),
SetupAnimationHelper.createFrameAnimatorWithDelay(mTvContentView,
- TV_FRAMES_2_BLUE_END, BLUE_SCREEN_HOLD_DURATION_MS),
+ TV_FRAMES_3_BLUE_END, BLUE_SCREEN_HOLD_DURATION_MS),
SetupAnimationHelper.createFrameAnimator(mArrowView,
- TV_FRAMES_2_ORANGE_ARROW),
+ TV_FRAMES_3_ORANGE_ARROW),
SetupAnimationHelper.createFrameAnimator(mTvContentView,
- TV_FRAMES_2_ORANGE_START));
- mArrowView.setVisibility(View.VISIBLE);
- break;
- case 2:
- mArrowView.setVisibility(View.GONE);
- animatorSet.playSequentially(
- SetupAnimationHelper.createFadeOutAnimator(mTvContentView, 333, true),
- SetupAnimationHelper.createFrameAnimator(mTvContentView,
- TV_FRAMES_3_START));
+ TV_FRAMES_3_ORANGE_START));
+ animatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mArrowView.setImageResource(TV_FRAMES_3_BLUE_ARROW[0]);
+ }
+ });
+ firstFrame = TV_FRAMES_3_BLUE_START[0];
break;
case 3:
- animatorSet.playSequentially(
- SetupAnimationHelper.createFadeOutAnimator(mTvContentView, 333, true),
+ default:
+ animatorSet.playSequentially(hideAnimator,
SetupAnimationHelper.createFrameAnimator(mTvContentView,
TV_FRAMES_4_START));
+ firstFrame = TV_FRAMES_4_START[0];
break;
}
+ final int firstImageResource = firstFrame;
+ hideAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // Shows the first frame of show animation when the hide animator is canceled.
+ mTvContentView.setImageResource(firstImageResource);
+ }
+ });
mAnimator = SetupAnimationHelper.applyAnimationTimeScale(animatorSet);
mAnimator.start();
+ mNeedToEndAnimator = false;
}
}
diff --git a/src/com/android/tv/onboarding/WelcomePageFragment.java b/src/com/android/tv/onboarding/WelcomePageFragment.java
deleted file mode 100644
index 28499f1d..00000000
--- a/src/com/android/tv/onboarding/WelcomePageFragment.java
+++ /dev/null
@@ -1,58 +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.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";
-
- public WelcomePageFragment() {
- enableFragmentTransition(FRAGMENT_ENTER_TRANSITION | FRAGMENT_EXIT_TRANSITION);
- }
-
- @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;
- }
-
- @Override
- protected int[] getParentIdsForDelay() {
- return new int[] {R.id.welcome_page_fragment_root};
- }
-}
diff --git a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
index 2db877f7..313b2dfa 100644
--- a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
+++ b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
@@ -29,6 +29,7 @@ import com.android.tv.ApplicationSingletons;
import com.android.tv.TvApplication;
import com.android.tv.analytics.Analytics;
import com.android.tv.analytics.Tracker;
+import com.android.tv.common.SharedPreferencesUtils;
/**
* Creates HDMI plug broadcast receiver, and reports AC3 passthrough capabilities to Google
@@ -36,7 +37,6 @@ import com.android.tv.analytics.Tracker;
* {@link #unregister} to stop.
*/
public final class AudioCapabilitiesReceiver {
- private static final String PREFS_NAME = "com.android.tv.audio_capabilities";
private static final String SETTINGS_KEY_AC3_PASSTHRU_REPORTED = "ac3_passthrough_reported";
private static final String SETTINGS_KEY_AC3_PASSTHRU_CAPABILITIES = "ac3_passthrough";
private static final String SETTINGS_KEY_AC3_REPORT_REVISION = "ac3_report_revision";
@@ -121,7 +121,8 @@ public final class AudioCapabilitiesReceiver {
}
private SharedPreferences getSharedPreferences() {
- return mContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ return mContext.getSharedPreferences(SharedPreferencesUtils.SHARED_PREF_AUDIO_CAPABILITIES,
+ Context.MODE_PRIVATE);
}
private boolean getBoolean(String key, boolean def) {
diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java
index 2b997c32..3cd6186c 100644
--- a/src/com/android/tv/receiver/BootCompletedReceiver.java
+++ b/src/com/android/tv/receiver/BootCompletedReceiver.java
@@ -25,6 +25,7 @@ import android.util.Log;
import com.android.tv.Features;
import com.android.tv.TvActivity;
+import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.dvr.DvrRecordingService;
import com.android.tv.recommendation.NotificationService;
import com.android.tv.util.OnboardingUtils;
@@ -58,8 +59,8 @@ public class BootCompletedReceiver extends BroadcastReceiver {
if (Features.UNHIDE.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.
+ // Enable the application if this is the first "unhide" feature is enabled just in
+ // case when the app has been disabled before.
PackageManager pm = context.getPackageManager();
ComponentName name = new ComponentName(context, TvActivity.class);
if (pm.getComponentEnabledSetting(name)
@@ -72,7 +73,7 @@ public class BootCompletedReceiver extends BroadcastReceiver {
}
// DVR
- if (Features.DVR.isEnabled(context)) {
+ if (CommonFeatures.DVR.isEnabled(context)) {
DvrRecordingService.startService(context);
}
}
diff --git a/src/com/android/tv/receiver/PackageIntentsReceiver.java b/src/com/android/tv/receiver/PackageIntentsReceiver.java
index cb35c87a..67f0529f 100644
--- a/src/com/android/tv/receiver/PackageIntentsReceiver.java
+++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java
@@ -24,7 +24,7 @@ import android.content.pm.PackageManager;
import com.android.tv.TvActivity;
import com.android.tv.TvApplication;
-import com.android.usbtuner.TunerSetupActivity;
+import com.android.usbtuner.setup.TunerSetupActivity;
import com.android.usbtuner.UsbTunerPreferences;
import com.android.usbtuner.tvinput.UsbTunerTvInputService;
diff --git a/src/com/android/tv/recommendation/NotificationService.java b/src/com/android/tv/recommendation/NotificationService.java
index 3ab67c8b..c6a0c3f6 100644
--- a/src/com/android/tv/recommendation/NotificationService.java
+++ b/src/com/android/tv/recommendation/NotificationService.java
@@ -34,30 +34,36 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseLongArray;
import android.view.View;
-import com.android.tv.R;
import com.android.tv.ApplicationSingletons;
+import com.android.tv.MainActivityWrapper.OnCurrentChannelChangeListener;
+import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.common.WeakHandler;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.util.BitmapUtils;
import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
+import com.android.tv.util.ImageLoader;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
+import java.util.ArrayList;
import java.util.List;
/**
* A local service for notify recommendation at home launcher.
*/
-public class NotificationService extends Service implements Recommender.Listener {
- private static final boolean DEBUG = false;
+public class NotificationService extends Service implements Recommender.Listener,
+ OnCurrentChannelChangeListener {
private static final String TAG = "NotificationService";
+ private static final boolean DEBUG = false;
public static final String ACTION_SHOW_RECOMMENDATION =
"com.android.tv.notification.ACTION_SHOW_RECOMMENDATION";
@@ -101,6 +107,8 @@ public class NotificationService extends Service implements Recommender.Listener
private int mCurrentNotificationCount;
private long[] mNotificationChannels;
+ private Channel mPlayingChannel;
+
private float mNotificationCardMaxWidth;
private float mNotificationCardHeight;
private int mCardImageHeight;
@@ -152,6 +160,16 @@ public class NotificationService extends Service implements Recommender.Listener
// Just called for early initialization.
appSingletons.getChannelDataManager();
appSingletons.getProgramDataManager();
+ appSingletons.getMainActivityWrapper().addOnCurrentChannelChangeListener(this);
+ }
+
+ @UiThread
+ @Override
+ public void onCurrentChannelChange(@Nullable Channel channel) {
+ if (DEBUG) Log.d(TAG, "onCurrentChannelChange");
+ mPlayingChannel = channel;
+ mHandler.removeMessages(MSG_SHOW_RECOMMENDATION);
+ mHandler.sendEmptyMessage(MSG_SHOW_RECOMMENDATION);
}
private void handleInitializeRecommender() {
@@ -195,11 +213,17 @@ public class NotificationService extends Service implements Recommender.Listener
@Override
public void onDestroy() {
- mRecommender.release();
- mRecommender = null;
- mHandlerThread.quit();
- mHandlerThread = null;
- mHandler = null;
+ TvApplication.getSingletons(this).getMainActivityWrapper()
+ .removeOnCurrentChannelChangeListener(this);
+ if (mRecommender != null) {
+ mRecommender.release();
+ mRecommender = null;
+ }
+ if (mHandlerThread != null) {
+ mHandlerThread.quit();
+ mHandlerThread = null;
+ mHandler = null;
+ }
super.onDestroy();
}
@@ -231,6 +255,7 @@ public class NotificationService extends Service implements Recommender.Listener
public void onRecommenderReady() {
if (DEBUG) Log.d(TAG, "onRecommendationReady");
if (mShowRecommendationAfterRecommenderReady) {
+ mHandler.removeMessages(MSG_SHOW_RECOMMENDATION);
mHandler.sendEmptyMessage(MSG_SHOW_RECOMMENDATION);
mShowRecommendationAfterRecommenderReady = false;
}
@@ -239,6 +264,13 @@ public class NotificationService extends Service implements Recommender.Listener
@Override
public void onRecommendationChanged() {
if (DEBUG) Log.d(TAG, "onRecommendationChanged");
+ // Update recommendation on the handler thread.
+ mHandler.removeMessages(MSG_SHOW_RECOMMENDATION);
+ mHandler.sendEmptyMessage(MSG_SHOW_RECOMMENDATION);
+ }
+
+ private void showRecommendation() {
+ if (DEBUG) Log.d(TAG, "showRecommendation");
SparseLongArray notificationChannels = new SparseLongArray();
for (int i = 0; i < NOTIFICATION_COUNT; ++i) {
if (mNotificationChannels[i] == Channel.INVALID_ID) {
@@ -246,7 +278,7 @@ public class NotificationService extends Service implements Recommender.Listener
}
notificationChannels.put(i, mNotificationChannels[i]);
}
- List<Channel> channels = mRecommender.recommendChannels();
+ List<Channel> channels = recommendChannels();
for (Channel c : channels) {
int index = notificationChannels.indexOfValue(c.getId());
if (index >= 0) {
@@ -261,13 +293,7 @@ public class NotificationService extends Service implements Recommender.Listener
mNotificationChannels[notificationId] = Channel.INVALID_ID;
--mCurrentNotificationCount;
}
- showRecommendation();
}
- }
-
- private void showRecommendation() {
- if (DEBUG) Log.d(TAG, "showRecommendation");
- List<Channel> channels = mRecommender.recommendChannels();
for (Channel c : channels) {
if (mCurrentNotificationCount >= NOTIFICATION_COUNT) {
break;
@@ -277,14 +303,13 @@ public class NotificationService extends Service implements Recommender.Listener
}
}
if (mCurrentNotificationCount < NOTIFICATION_COUNT) {
- Message msg = mHandler.obtainMessage(MSG_SHOW_RECOMMENDATION);
- mHandler.sendMessageDelayed(msg, RECOMMENDATION_RETRY_TIME_MS);
+ mHandler.sendEmptyMessageDelayed(MSG_SHOW_RECOMMENDATION, RECOMMENDATION_RETRY_TIME_MS);
}
}
private void changeRecommendation(int notificationId) {
if (DEBUG) Log.d(TAG, "changeRecommendation");
- List<Channel> channels = mRecommender.recommendChannels();
+ List<Channel> channels = recommendChannels();
if (mNotificationChannels[notificationId] != Channel.INVALID_ID) {
mNotificationChannels[notificationId] = Channel.INVALID_ID;
--mCurrentNotificationCount;
@@ -299,6 +324,15 @@ public class NotificationService extends Service implements Recommender.Listener
mNotificationManager.cancel(NOTIFY_TAG, notificationId);
}
+ private List<Channel> recommendChannels() {
+ List channels = mRecommender.recommendChannels();
+ if (channels.contains(mPlayingChannel)) {
+ channels = new ArrayList<>(channels);
+ channels.remove(mPlayingChannel);
+ }
+ return channels;
+ }
+
private void hideAllRecommendation() {
for (int i = 0; i < NOTIFICATION_COUNT; ++i) {
if (mNotificationChannels[i] != Channel.INVALID_ID) {
@@ -319,9 +353,6 @@ public class NotificationService extends Service implements Recommender.Listener
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);
// TODO: Move some checking logic into TvRecommendation.
String inputId = Utils.getInputIdForChannel(this, channel.getId());
@@ -361,43 +392,9 @@ public class NotificationService extends Service implements Recommender.Listener
final Bitmap posterArtBitmap = posterArtBitmapInfo.bitmap;
channel.loadBitmap(this, Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, mChannelLogoMaxWidth,
- mChannelLogoMaxHeight, new Channel.LoadImageCallback() {
- @Override
- public void onLoadImageFinished(Channel channel, int type, Bitmap channelLogo) {
- // 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 + " " +
- (TextUtils.isEmpty(channelDisplayName)
- ? channel.getDisplayNumber() : channelDisplayName))
- .setContentInfo(channelDisplayName)
- .setAutoCancel(true)
- .setLargeIcon(largeIconBitmap)
- .setSmallIcon(R.drawable.ic_launcher_s)
- .setCategory(Notification.CATEGORY_RECOMMENDATION)
- .setProgress((programProgress > 0) ? 100 : 0,
- programProgress,
- false)
- .setSortKey(mRecommender.getChannelSortKey(channelId))
- .build();
- 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());
- }
- mNotificationManager.notify(NOTIFY_TAG, notificationId, notification);
- Message msg = mHandler.obtainMessage(
- MSG_UPDATE_RECOMMENDATION, notificationId, 0, channel);
- mHandler.sendMessageDelayed(msg,
- programDurationMs / MAX_PROGRAM_UPDATE_COUNT);
- }
- });
+ mChannelLogoMaxHeight,
+ createChannelLogoCallback(this, notificationId, inputDisplayName, channel, program,
+ posterArtBitmap));
if (mNotificationChannels[notificationId] == Channel.INVALID_ID) {
++mCurrentNotificationCount;
@@ -407,6 +404,55 @@ public class NotificationService extends Service implements Recommender.Listener
return true;
}
+ @NonNull
+ private static ImageLoader.ImageLoaderCallback<NotificationService> createChannelLogoCallback(
+ NotificationService service, final int notificationId, final String inputDisplayName,
+ final Channel channel, final Program program, final Bitmap posterArtBitmap) {
+ return new ImageLoader.ImageLoaderCallback<NotificationService>(service) {
+ @Override
+ public void onBitmapLoaded(NotificationService service, Bitmap channelLogo) {
+ service.sendNotification(notificationId, channelLogo, channel, posterArtBitmap,
+ program, inputDisplayName);
+ }
+ };
+ }
+
+ private void sendNotification(int notificationId, Bitmap channelLogo, Channel channel,
+ Bitmap posterArtBitmap, Program program, String inputDisplayName1) {
+
+ final long programDurationMs = program.getEndTimeUtcMillis() - program
+ .getStartTimeUtcMillis();
+ long programLeftTimsMs = program.getEndTimeUtcMillis() - System.currentTimeMillis();
+ final int programProgress = (programDurationMs <= 0) ? -1
+ : 100 - (int) (programLeftTimsMs * 100 / programDurationMs);
+ 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);
+
+ // 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(this)
+ .setContentIntent(notificationIntent).setContentTitle(program.getTitle())
+ .setContentText(inputDisplayName1 + " " +
+ (TextUtils.isEmpty(channelDisplayName) ? channel.getDisplayNumber()
+ : channelDisplayName)).setContentInfo(channelDisplayName)
+ .setAutoCancel(true).setLargeIcon(largeIconBitmap)
+ .setSmallIcon(R.drawable.ic_launcher_s)
+ .setCategory(Notification.CATEGORY_RECOMMENDATION)
+ .setProgress((programProgress > 0) ? 100 : 0, programProgress, false)
+ .setSortKey(mRecommender.getChannelSortKey(channel.getId())).build();
+ 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());
+ }
+ mNotificationManager.notify(NOTIFY_TAG, notificationId, notification);
+ Message msg = mHandler.obtainMessage(MSG_UPDATE_RECOMMENDATION, notificationId, 0, channel);
+ mHandler.sendMessageDelayed(msg, programDurationMs / MAX_PROGRAM_UPDATE_COUNT);
+ }
+
private Bitmap overlayChannelLogo(Bitmap logo, Bitmap background) {
Bitmap result = BitmapUtils.scaleBitmap(
background, Integer.MAX_VALUE, mCardImageHeight);
diff --git a/src/com/android/tv/search/DataManagerSearch.java b/src/com/android/tv/search/DataManagerSearch.java
index 88c69c53..d26ae334 100644
--- a/src/com/android/tv/search/DataManagerSearch.java
+++ b/src/com/android/tv/search/DataManagerSearch.java
@@ -22,7 +22,7 @@ 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.support.annotation.MainThread;
import android.text.TextUtils;
import android.util.Log;
@@ -87,7 +87,7 @@ public class DataManagerSearch implements SearchInterface {
}
}
- @UiThread
+ @MainThread
private List<SearchResult> searchFromDataManagers(String query, int limit, int action) {
List<SearchResult> results = new ArrayList<>();
if (!mChannelDataManager.isDbLoadFinished()) {
diff --git a/src/com/android/tv/search/LocalSearchProvider.java b/src/com/android/tv/search/LocalSearchProvider.java
index 3cc21ace..7edb07dc 100644
--- a/src/com/android/tv/search/LocalSearchProvider.java
+++ b/src/com/android/tv/search/LocalSearchProvider.java
@@ -64,8 +64,6 @@ public class LocalSearchProvider extends ContentProvider {
static final String SUGGEST_PARAMETER_ACTION = "action";
static final int DEFAULT_SEARCH_ACTION = SearchInterface.ACTION_TYPE_AMBIGUOUS;
- private SearchInterface mSearch;
-
@Override
public boolean onCreate() {
return true;
@@ -78,10 +76,11 @@ public class LocalSearchProvider extends ContentProvider {
Log.d(TAG, "query(" + uri + ", " + Arrays.toString(projection) + ", " + selection + ", "
+ Arrays.toString(selectionArgs) + ", " + sortOrder + ")");
}
+ SearchInterface search;
if (PermissionUtils.hasAccessAllEpg(getContext())) {
- mSearch = new TvProviderSearch(getContext());
+ search = new TvProviderSearch(getContext());
} else {
- mSearch = new DataManagerSearch(getContext());
+ search = new DataManagerSearch(getContext());
}
String query = uri.getLastPathSegment();
int limit = DEFAULT_SEARCH_LIMIT;
@@ -94,7 +93,7 @@ public class LocalSearchProvider extends ContentProvider {
}
List<SearchResult> results = new ArrayList<>();
if (!TextUtils.isEmpty(query)) {
- results.addAll(mSearch.search(query, limit, action));
+ results.addAll(search.search(query, limit, action));
}
return createSuggestionsCursor(results);
}
diff --git a/src/com/android/tv/search/ProgramGuideSearchFragment.java b/src/com/android/tv/search/ProgramGuideSearchFragment.java
index 7d6efcb3..87eec68e 100644
--- a/src/com/android/tv/search/ProgramGuideSearchFragment.java
+++ b/src/com/android/tv/search/ProgramGuideSearchFragment.java
@@ -71,21 +71,14 @@ public class ProgramGuideSearchFragment extends SearchFragment {
@Override
public void onBindViewHolder(ViewHolder viewHolder, Object o) {
- final ImageCardView cardView = (ImageCardView) viewHolder.view;
+ ImageCardView cardView = (ImageCardView) viewHolder.view;
LocalSearchProvider.SearchResult result = (LocalSearchProvider.SearchResult) o;
if (DEBUG) Log.d(TAG, "onBindViewHolder result:" + result);
cardView.setTitleText(result.title);
if (!TextUtils.isEmpty(result.imageUri)) {
- ImageLoader.loadBitmap(mMainActivity, result.imageUri,
- mMainCardWidth, mMainCardHeight,
- new ImageLoader.ImageLoaderCallback() {
- @Override
- public void onBitmapLoaded(Bitmap bitmap) {
- cardView.setMainImage(
- new BitmapDrawable(mMainActivity.getResources(), bitmap));
- }
- });
+ ImageLoader.loadBitmap(mMainActivity, result.imageUri, mMainCardWidth,
+ mMainCardHeight, createImageLoaderCallback(cardView));
} else {
cardView.setMainImage(mMainActivity.getDrawable(R.drawable.ic_launcher));
}
@@ -97,6 +90,17 @@ public class ProgramGuideSearchFragment extends SearchFragment {
}
};
+ private static ImageLoader.ImageLoaderCallback<ImageCardView> createImageLoaderCallback(
+ ImageCardView cardView) {
+ return new ImageLoader.ImageLoaderCallback<ImageCardView>(cardView) {
+ @Override
+ public void onBitmapLoaded(ImageCardView cardView, Bitmap bitmap) {
+ cardView.setMainImage(
+ new BitmapDrawable(cardView.getContext().getResources(), bitmap));
+ }
+ };
+ }
+
private final SearchResultProvider mSearchResultProvider = new SearchResultProvider() {
@Override
public ObjectAdapter getResultsAdapter() {
diff --git a/src/com/android/tv/search/TvProviderSearch.java b/src/com/android/tv/search/TvProviderSearch.java
index a5ad00ff..bd4ae5e5 100644
--- a/src/com/android/tv/search/TvProviderSearch.java
+++ b/src/com/android/tv/search/TvProviderSearch.java
@@ -32,6 +32,7 @@ import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
+import com.android.tv.common.TvContentRatingCache;
import com.android.tv.search.LocalSearchProvider.SearchResult;
import com.android.tv.util.PermissionUtils;
import com.android.tv.util.Utils;
@@ -61,6 +62,7 @@ public class TvProviderSearch implements SearchInterface {
private final Context mContext;
private final ContentResolver mContentResolver;
private final TvInputManager mTvInputManager;
+ private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance();
TvProviderSearch(Context context) {
mContext = context;
@@ -403,17 +405,15 @@ public class TvProviderSearch implements SearchInterface {
}
private boolean isRatingBlocked(String ratings) {
- if (ratings == null) {
+ if (TextUtils.isEmpty(ratings) || !mTvInputManager.isParentalControlsEnabled()) {
return false;
}
- for (String rating : ratings.split("\\s*,\\s*")) {
- try {
- if (mTvInputManager.isParentalControlsEnabled() && mTvInputManager.isRatingBlocked(
- TvContentRating.unflattenFromString(rating))) {
+ TvContentRating[] ratingArray = mTvContentRatingCache.getRatings(ratings);
+ if (ratingArray != null) {
+ for (TvContentRating r : ratingArray) {
+ if (mTvInputManager.isRatingBlocked(r)) {
return true;
}
- } catch (IllegalArgumentException e) {
- // Do nothing.
}
}
return false;
diff --git a/src/com/android/tv/ui/AppLayerTvView.java b/src/com/android/tv/ui/AppLayerTvView.java
index 23ac5392..befa004c 100644
--- a/src/com/android/tv/ui/AppLayerTvView.java
+++ b/src/com/android/tv/ui/AppLayerTvView.java
@@ -16,7 +16,7 @@
package com.android.tv.ui;
-import com.android.tv.common.dvr.DvrTvView;
+import com.android.tv.common.recording.PlaybackTvView;
import android.content.Context;
import android.util.AttributeSet;
@@ -30,7 +30,7 @@ import android.util.AttributeSet;
* TODO: remove this class once the TvView.setMain() is revisited.
* </p>
*/
-public class AppLayerTvView extends DvrTvView {
+public class AppLayerTvView extends PlaybackTvView {
public AppLayerTvView(Context context) {
super(context);
}
diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java
index bf8e69c7..17ac8f3b 100644
--- a/src/com/android/tv/ui/ChannelBannerView.java
+++ b/src/com/android/tv/ui/ChannelBannerView.java
@@ -16,6 +16,8 @@
package com.android.tv.ui;
+import static com.android.tv.util.ImageLoader.ImageLoaderCallback;
+
import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.animation.AnimatorListenerAdapter;
@@ -29,6 +31,7 @@ import android.media.tv.TvContract;
import android.media.tv.TvInputInfo;
import android.net.Uri;
import android.os.Handler;
+import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
@@ -62,8 +65,7 @@ import java.util.Objects;
/**
* A view to render channel banner.
*/
-public class ChannelBannerView extends FrameLayout implements Channel.LoadImageCallback,
- TvTransitionManager.TransitionLayout {
+public class ChannelBannerView extends FrameLayout implements TvTransitionManager.TransitionLayout {
/**
* Show all information at the channel banner.
@@ -128,7 +130,8 @@ public class ChannelBannerView extends FrameLayout implements Channel.LoadImageC
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);
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
}
};
private final long mShowDurationMillis;
@@ -407,8 +410,7 @@ public class ChannelBannerView extends FrameLayout implements Channel.LoadImageC
TvInputInfo info = mMainActivity.getTvInputManagerHelper().getTvInputInfo(
mCurrentChannel.getInputId());
- if (info == null ||
- !ImageLoader.loadBitmap(createTvInputLogoLoaderCallback(info),
+ if (info == null || !ImageLoader.loadBitmap(createTvInputLogoLoaderCallback(info, this),
new LoadTvInputLogoTask(getContext(), ImageCache.getInstance(), info))) {
mTvInputLogoImageView.setVisibility(View.GONE);
mTvInputLogoImageView.setImageDrawable(null);
@@ -416,17 +418,23 @@ public class ChannelBannerView extends FrameLayout implements Channel.LoadImageC
mChannelLogoImageView.setImageBitmap(null);
mChannelLogoImageView.setVisibility(View.GONE);
mCurrentChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO,
- mChannelLogoImageViewWidth, mChannelLogoImageViewHeight, this);
+ mChannelLogoImageViewWidth, mChannelLogoImageViewHeight,
+ createChannelLogoCallback(this, mCurrentChannel));
+ }
+
+ private void updateTvInputLogo(Bitmap bitmap) {
+ mTvInputLogoImageView.setVisibility(View.VISIBLE);
+ mTvInputLogoImageView.setImageBitmap(bitmap);
}
- private ImageLoader.ImageLoaderCallback createTvInputLogoLoaderCallback(
- final TvInputInfo info) {
- return new ImageLoader.ImageLoaderCallback() {
+ private static ImageLoaderCallback<ChannelBannerView> createTvInputLogoLoaderCallback(
+ final TvInputInfo info, ChannelBannerView channelBannerView) {
+ return new ImageLoaderCallback<ChannelBannerView>(channelBannerView) {
@Override
- public void onBitmapLoaded(Bitmap bitmap) {
- if (bitmap != null && info.getId().equals(mCurrentChannel.getInputId())) {
- mTvInputLogoImageView.setVisibility(View.VISIBLE);
- mTvInputLogoImageView.setImageBitmap(bitmap);
+ public void onBitmapLoaded(ChannelBannerView channelBannerView, Bitmap bitmap) {
+ if (bitmap != null && info.getId()
+ .equals(channelBannerView.mCurrentChannel.getInputId())) {
+ channelBannerView.updateTvInputLogo(bitmap);
}
}
};
@@ -458,12 +466,21 @@ public class ChannelBannerView extends FrameLayout implements Channel.LoadImageC
}
}
- @Override
- public void onLoadImageFinished(Channel channel, int type, Bitmap logo) {
- if (channel != mCurrentChannel) {
- // The logo is obsolete.
- return;
- }
+ private static ImageLoaderCallback<ChannelBannerView> createChannelLogoCallback(
+ ChannelBannerView channelBannerView, final Channel channel) {
+ return new ImageLoaderCallback<ChannelBannerView>(channelBannerView) {
+ @Override
+ public void onBitmapLoaded(ChannelBannerView view, @Nullable Bitmap logo) {
+ if (channel != view.mCurrentChannel) {
+ // The logo is obsolete.
+ return;
+ }
+ view.updateLogo(logo);
+ }
+ };
+ }
+
+ private void updateLogo(@Nullable Bitmap logo) {
if (logo == null) {
// Need to update the text size of the program text view depending on the channel logo.
updateProgramTextView(mLastUpdatedProgram);
diff --git a/src/com/android/tv/ui/InputBannerView.java b/src/com/android/tv/ui/InputBannerView.java
index 649331f4..4e254c62 100644
--- a/src/com/android/tv/ui/InputBannerView.java
+++ b/src/com/android/tv/ui/InputBannerView.java
@@ -38,7 +38,8 @@ public class InputBannerView extends LinearLayout implements TvTransitionManager
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);
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
}
};
diff --git a/src/com/android/tv/ui/KeypadChannelSwitchView.java b/src/com/android/tv/ui/KeypadChannelSwitchView.java
index 140ba533..cf43fc9b 100644
--- a/src/com/android/tv/ui/KeypadChannelSwitchView.java
+++ b/src/com/android/tv/ui/KeypadChannelSwitchView.java
@@ -84,7 +84,8 @@ public class KeypadChannelSwitchView extends LinearLayout implements
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);
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
}
}
};
@@ -127,6 +128,7 @@ public class KeypadChannelSwitchView extends LinearLayout implements
@Override
protected void onFinishInflate(){
+ super.onFinishInflate();
mChannelNumberView = (TextView) findViewById(R.id.channel_number);
mChannelItemListView = (ListView) findViewById(R.id.channel_list);
mChannelItemListView.setAdapter(mAdapter);
diff --git a/src/com/android/tv/ui/SetupView.java b/src/com/android/tv/ui/SetupView.java
deleted file mode 100644
index 95a9f28e..00000000
--- a/src/com/android/tv/ui/SetupView.java
+++ /dev/null
@@ -1,419 +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.ui;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.animation.TimeInterpolator;
-import android.app.Dialog;
-import android.content.Context;
-import android.media.tv.TvInputInfo;
-import android.media.tv.TvInputManager.TvInputCallback;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.support.v7.widget.RecyclerView;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-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.data.ChannelDataManager;
-import com.android.tv.data.TvInputNewComparator;
-import com.android.tv.util.SetupUtils;
-import com.android.tv.util.TvInputManagerHelper;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-public class SetupView extends FullscreenDialogView {
- private static final String TAG = "SetupView";
- private static final boolean DEBUG = false;
-
- private static final int FINISH_ACTIVITY_DELAY_MS = 200;
- private static final int REFRESH_DELAY_MS_AFTER_WINDOW_FOCUS_GAINED = 200;
-
- private static final long ANIMATION_START_DELAY = 25;
-
- private VerticalGridView mInputView;
- private ChannelDataManager mChannelDataManager;
- private TvInputManagerHelper mInputManager;
- private List<TvInputInfo> mInputList;
- // mInputList[0:mKnownInputStartIndex - 1] are new inputs.
- // And mInputList[mKnownInputStartIndex:end] are inputs which have been shown in SetupView.
- private int mKnownInputStartIndex;
- private boolean mShowDivider;
- private SetupAdapter mAdapter;
- private boolean mClosing;
- private boolean mInitialized;
- private SetupUtils mSetupUtils;
- private boolean mNeedIntroDialog;
- private final int mEnterTranslationX;
- private final int mExitTranslationX;
- private Animator mEnterAnimator;
-
- private final TvInputCallback mInputCallback = new TvInputCallback() {
- @Override
- public void onInputAdded(String inputId) {
- if (DEBUG) {
- Log.d(TAG, "onInputAdded: " + inputId);
- }
- if (!mInitialized) {
- return;
- }
- updateInputList();
- }
-
- @Override
- public void onInputRemoved(String inputId) {
- if (DEBUG) {
- Log.d(TAG, "onInputRemoved: " + inputId);
- }
- if (!mInitialized) {
- return;
- }
- updateInputList();
- }
- };
- private final ChannelDataManager.Listener mChannelDataListener =
- new ChannelDataManager.Listener() {
- @Override
- public void onLoadFinished() { }
-
- @Override
- public void onChannelListUpdated() {
- if (mAdapter != null) {
- mAdapter.notifyDataSetChanged();
- }
- }
-
- @Override
- public void onChannelBrowsableChanged() {
- if (mAdapter != null) {
- mAdapter.notifyDataSetChanged();
- }
- }
- };
-
- public SetupView(Context context) {
- this(context, null, 0);
- }
-
- public SetupView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public SetupView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- mEnterTranslationX = context.getResources().getInteger(
- R.integer.fullscreen_dialog_enter_translation_x);
- mExitTranslationX = context.getResources().getInteger(
- R.integer.fullscreen_dialog_exit_translation_x);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- TextView titleView = (TextView) findViewById(R.id.setup_title);
- titleView.setText(R.string.setup_title);
- TextView descriptionView = (TextView) findViewById(R.id.setup_description);
- descriptionView.setText(R.string.setup_description);
- mInputView = (VerticalGridView) findViewById(R.id.input_list);
- TypedValue outValue = new TypedValue();
- getResources().getValue(R.dimen.setup_item_window_alignment_offset_percent, outValue, true);
- mInputView.setWindowAlignmentOffsetPercent(outValue.getFloat());
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
- mInputManager.addCallback(mInputCallback);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mInputManager.removeCallback(mInputCallback);
- }
-
- @Override
- public boolean dispatchKeyEvent(KeyEvent event) {
- return mClosing || super.dispatchKeyEvent(event);
- }
-
- @Override
- public void onWindowFocusChanged(boolean hasWindowFocus) {
- if (hasWindowFocus && mAdapter != null) {
- // Without the following delay, the channel count description is sometimes
- // changed twice by this method and mChannelDataListener.
- postDelayed(new Runnable() {
- @Override
- public void run() {
- // When channel count is still 0 after setup, the description should be changed
- // from "Not set up" to "No channels".
- if (mAdapter.getItemCount() != 0) {
- mAdapter.notifyItemRangeChanged(0, mAdapter.getItemCount());
- }
- }
- }, REFRESH_DELAY_MS_AFTER_WINDOW_FOCUS_GAINED);
- }
- }
-
- /**
- * Initializes SetupView.
- */
- @Override
- public void initialize(MainActivity activity, Dialog dialog) {
- super.initialize(activity, dialog);
- if (mInitialized) {
- throw new IllegalStateException("initialize() is called more than once");
- }
- mInitialized = true;
- mInputManager = getActivity().getTvInputManagerHelper();
- mChannelDataManager = getActivity().getChannelDataManager();
- mSetupUtils = SetupUtils.getInstance(activity);
- mNeedIntroDialog = mSetupUtils.isFirstTune();
- mAdapter = new SetupAdapter();
- mInputView.setAdapter(mAdapter);
- mChannelDataManager.addListener(mChannelDataListener);
- updateInputList();
- }
-
- private void updateInputList() {
- mInputList = new ArrayList<>();
- mKnownInputStartIndex = 0;
- mInputList = mInputManager.getTvInputInfos(true, true);
- Collections.sort(mInputList, new TvInputNewComparator(mSetupUtils, mInputManager));
- for (TvInputInfo input : mInputList) {
- if (mSetupUtils.isNewInput(input.getId())) {
- mSetupUtils.markAsKnownInput(input.getId());
- ++mKnownInputStartIndex;
- }
- }
- mShowDivider = mKnownInputStartIndex != 0 && mKnownInputStartIndex != mInputList.size();
- mNeedIntroDialog = mSetupUtils.isFirstTune();
- mAdapter.notifyDataSetChanged();
- }
-
- /**
- * Called when the DialogFragment including this view is destroyed.
- */
- @Override
- public void onDestroy() {
- mChannelDataManager.removeListener(mChannelDataListener);
- }
-
- @Override
- protected void dismiss() {
- mClosing = true;
- if (mNeedIntroDialog) {
- LayoutInflater inflater = LayoutInflater.from(getActivity());
- IntroView v = (IntroView) inflater.inflate(R.layout.intro_dialog, null);
- transitionTo(v);
- } else {
- super.dismiss();
- }
- }
- /**
- * Called when the back key is pressed.
- */
- @Override
- public void onBackPressed() {
- if (mChannelDataManager.getChannelCount() == 0) {
- // If there is no channel, we finish the activity rather than closing just the view.
- getActivity().finish();
- }
- dismiss();
- }
-
- @Override
- protected void onStartEnterAnimation(final TimeInterpolator interpolator, final long duration) {
- List<Animator> animatorList = new ArrayList<>();
- View leftPanel = findViewById(R.id.setup_left);
- leftPanel.setAlpha(0);
- leftPanel.setTranslationX(mEnterTranslationX);
- animatorList.add(buildEnterAnimator(leftPanel, duration, 0, interpolator));
-
- for (int i = 0; i < mInputView.getChildCount(); ++i) {
- View itemView = mInputView.getChildAt(i);
- itemView.setAlpha(0);
- itemView.setTranslationX(mEnterTranslationX);
- int itemPosition = mInputView.getChildAdapterPosition(itemView);
- animatorList.add(buildEnterAnimator(itemView, duration,
- ANIMATION_START_DELAY * (itemPosition + 1), interpolator));
- }
- AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(animatorList);
- mEnterAnimator = animatorSet;
- mEnterAnimator.start();
- }
-
- private Animator buildEnterAnimator(View v, long duration, long startDelay,
- TimeInterpolator interpolator) {
- Animator animator = ObjectAnimator.ofPropertyValuesHolder(v,
- PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1.0f),
- PropertyValuesHolder.ofFloat(View.TRANSLATION_X, mEnterTranslationX, 0));
- animator.setStartDelay(startDelay);
- animator.setDuration(duration);
- animator.setInterpolator(interpolator);
- animator.addListener(new HardwareLayerAnimatorListenerAdapter(v));
- return animator;
- }
-
- @Override
- protected void onStartExitAnimation(TimeInterpolator interpolator, long duration,
- final Runnable onAnimationEnded) {
- if (mEnterAnimator != null && mEnterAnimator.isRunning()) {
- mEnterAnimator.cancel();
- }
- List<Animator> animatorList = new ArrayList<>();
- animatorList.add(
- buildExitAnimator(findViewById(R.id.setup_left), duration, 0, interpolator));
- for (int i = 0; i < mInputView.getChildCount(); ++i) {
- View itemView = mInputView.getChildAt(i);
- int itemPosition = mInputView.getChildAdapterPosition(itemView);
- animatorList.add(buildExitAnimator(itemView, duration,
- ANIMATION_START_DELAY * (itemPosition + 1), interpolator));
- }
- AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(animatorList);
- animatorSet.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- onAnimationEnded.run();
- }
- });
- animatorSet.start();
- }
-
- private Animator buildExitAnimator(View v, long duration, long startDelay,
- TimeInterpolator interpolator) {
- Animator animator = ObjectAnimator.ofPropertyValuesHolder(v,
- PropertyValuesHolder.ofFloat(View.ALPHA, v.getAlpha(), 0f),
- PropertyValuesHolder.ofFloat(View.TRANSLATION_X,
- v.getTranslationX(), mExitTranslationX));
- animator.setStartDelay(startDelay);
- animator.setDuration(duration);
- animator.setInterpolator(interpolator);
- animator.addListener(new HardwareLayerAnimatorListenerAdapter(v));
- return animator;
- }
-
- private class SetupAdapter extends RecyclerView.Adapter<MyViewHolder> {
- @Override
- public int getItemViewType(int position) {
- if (mShowDivider && position == mKnownInputStartIndex) {
- return R.layout.setup_item_divider;
- } else if (position == getItemCount() - 1) {
- return R.layout.setup_item_action;
- } else {
- return R.layout.setup_item_input;
- }
- }
-
- @Override
- public int getItemCount() {
- if (mInputList == null) {
- return 1;
- }
- return mInputList.size() + 1 + (mShowDivider ? 1 : 0);
- }
-
- @Override
- public void onBindViewHolder(final MyViewHolder viewHolder, int position) {
- if (position == getItemCount() - 1) {
- final boolean closeActivity = mChannelDataManager.getChannelCount() == 0;
- viewHolder.mTitle.setText(R.string.setup_done_button_label);
- viewHolder.itemView.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- mClosing = true;
- if (closeActivity) {
- // To wait completing ripple animation, finish() is called
- // FINISH_ACTIVITY_DELAY_MS later.
- mNeedIntroDialog = false;
- postDelayed(new Runnable() {
- @Override
- public void run() {
- getActivity().finish();
- }
- }, FINISH_ACTIVITY_DELAY_MS);
- } else {
- dismiss();
- }
- }
- });
- } else {
- if (mShowDivider) {
- if (position == mKnownInputStartIndex) {
- // This view is a divider.
- return;
- } else if (position > mKnownInputStartIndex) {
- --position;
- }
- }
- final TvInputInfo input = mInputList.get(position);
- viewHolder.mTitle.setText(input.loadLabel(getContext()));
- int channelCount = mChannelDataManager.getChannelCountForInput(input.getId());
- if (mSetupUtils.isSetupDone(input.getId())) {
- if (channelCount == 0) {
- viewHolder.mDescription.setText(R.string.setup_input_no_channels);
- } else {
- viewHolder.mDescription.setText(getResources().getQuantityString(
- R.plurals.setup_input_channels, channelCount, channelCount));
- }
- } else if (position >= mKnownInputStartIndex) {
- viewHolder.mDescription.setText(R.string.channel_description_setup_now);
- } else {
- viewHolder.mDescription.setText(R.string.setup_input_new);
- }
- viewHolder.itemView.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- getActivity().startSetupActivity(input, true);
- }
- });
- }
- }
-
- @Override
- public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- View itemView = LayoutInflater.from(parent.getContext()).inflate(viewType, parent,
- false);
- return new MyViewHolder(itemView);
- }
- }
-
- private static class MyViewHolder extends RecyclerView.ViewHolder {
- final TextView mTitle;
- final TextView mDescription;
-
- public MyViewHolder(View itemView) {
- super(itemView);
- mTitle = (TextView) itemView.findViewById(R.id.title);
- mDescription = (TextView) itemView.findViewById(R.id.description);
- }
- }
-}
diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java
index fe185b2e..806dc6ef 100644
--- a/src/com/android/tv/ui/TunableTvView.java
+++ b/src/com/android/tv/ui/TunableTvView.java
@@ -21,6 +21,7 @@ import android.animation.AnimatorInflater;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.PlaybackParams;
@@ -28,10 +29,10 @@ import android.media.tv.TvContentRating;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvTrackInfo;
-import android.media.tv.TvView;
import android.media.tv.TvView.OnUnhandledInputEventListener;
import android.media.tv.TvView.TvInputCallback;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
@@ -47,12 +48,13 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
-import com.android.tv.R;
import com.android.tv.ApplicationSingletons;
+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.common.TvCommonConstants;
+import com.android.tv.common.recording.PlaybackTvView;
+import com.android.tv.common.recording.RecordingUtils;
import com.android.tv.data.Channel;
import com.android.tv.data.StreamInfo;
import com.android.tv.data.WatchedHistoryManager;
@@ -311,6 +313,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
@Override
+ @TargetApi(Build.VERSION_CODES.M)
public void onTimeShiftStatusChanged(String inputId, int status) {
setTimeShiftAvailable(status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE);
}
@@ -468,6 +471,18 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
/**
+ * Plays a recording.
+ */
+ public boolean playRecording(String inputId, Uri recordingUri, OnTuneListener listener) {
+ // Create a dummy channel.
+ Channel channel = new Channel.Builder()
+ .setId(0)
+ .setInputId(inputId)
+ .build();
+ return tuneTo(channel, RecordingUtils.buildMediaUri(recordingUri), listener);
+ }
+
+ /**
* Tunes to a channel with the {@code channelId}.
*
* @param params extra data to send it to TIS and store the data in TIMS.
@@ -517,7 +532,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
mHasClosedCaption = false;
mTvView.setCallback(mCallback);
mTimeShiftCurrentPositionMs = INVALID_TIME;
- if (TvCommonConstants.HAS_TIME_SHIFT_API) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// To reduce the IPCs, unregister the callback here and register it when necessary.
mTvView.setTimeShiftPositionCallback(null);
}
@@ -1074,12 +1089,12 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
private void setTimeShiftAvailable(boolean isTimeShiftAvailable) {
- if (!TvCommonConstants.HAS_TIME_SHIFT_API || mTimeShiftAvailable == isTimeShiftAvailable) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || mTimeShiftAvailable == isTimeShiftAvailable) {
return;
}
mTimeShiftAvailable = isTimeShiftAvailable;
if (isTimeShiftAvailable) {
- mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() {
+ mTvView.setTimeShiftPositionCallback(new PlaybackTvView.TimeShiftPositionCallback2() {
@Override
public void onTimeShiftStartPositionChanged(String inputId, long timeMs) {
if (mTimeShiftListener != null && mCurrentChannel != null
@@ -1092,6 +1107,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) {
mTimeShiftCurrentPositionMs = timeMs;
}
+
+ @Override
+ public void onTimeShiftEndPositionChanged(String inputId, long timeMs) {
+ if (mTimeShiftListener != null && mCurrentChannel != null
+ && mCurrentChannel.getInputId().equals(inputId)) {
+ mTimeShiftListener.onRecordEndTimeChanged(timeMs);
+ }
+ }
});
} else {
mTvView.setTimeShiftPositionCallback(null);
@@ -1122,7 +1145,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
* Plays the media, if the current input supports time-shifting.
*/
public void timeshiftPlay() {
- if (!TvCommonConstants.HAS_TIME_SHIFT_API) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Log.w(TAG, "Time shifting is not supported in this platform.");
return;
}
@@ -1139,7 +1162,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
* Pauses the media, if the current input supports time-shifting.
*/
public void timeshiftPause() {
- if (!TvCommonConstants.HAS_TIME_SHIFT_API) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Log.w(TAG, "Time shifting is not supported in this platform.");
return;
}
@@ -1158,20 +1181,19 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
* @param speed The speed to rewind the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x.
*/
public void timeshiftRewind(int speed) {
- if (!TvCommonConstants.HAS_TIME_SHIFT_API) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Log.w(TAG, "Time shifting is not supported in this platform.");
- return;
- }
- if (!isTimeShiftAvailable()) {
+ } else if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
+ } else {
+ if (speed <= 0) {
+ throw new IllegalArgumentException("The speed should be a positive integer.");
+ }
+ mTimeShiftState = TIME_SHIFT_STATE_REWIND;
+ PlaybackParams params = new PlaybackParams();
+ params.setSpeed(speed * -1);
+ mTvView.timeShiftSetPlaybackParams(params);
}
- if (speed <= 0) {
- throw new IllegalArgumentException("The speed should be a positive integer.");
- }
- mTimeShiftState = TIME_SHIFT_STATE_REWIND;
- PlaybackParams params = new PlaybackParams();
- params.setSpeed(speed * -1);
- mTvView.timeShiftSetPlaybackParams(params);
}
/**
@@ -1180,20 +1202,19 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
* @param speed The speed to forward the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x.
*/
public void timeshiftFastForward(int speed) {
- if (!TvCommonConstants.HAS_TIME_SHIFT_API) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Log.w(TAG, "Time shifting is not supported in this platform.");
- return;
- }
- if (!isTimeShiftAvailable()) {
+ } else if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
+ } else {
+ if (speed <= 0) {
+ throw new IllegalArgumentException("The speed should be a positive integer.");
+ }
+ mTimeShiftState = TIME_SHIFT_STATE_FAST_FORWARD;
+ PlaybackParams params = new PlaybackParams();
+ params.setSpeed(speed);
+ mTvView.timeShiftSetPlaybackParams(params);
}
- if (speed <= 0) {
- throw new IllegalArgumentException("The speed should be a positive integer.");
- }
- mTimeShiftState = TIME_SHIFT_STATE_FAST_FORWARD;
- PlaybackParams params = new PlaybackParams();
- params.setSpeed(speed);
- mTvView.timeShiftSetPlaybackParams(params);
}
/**
@@ -1202,7 +1223,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
* @param timeMs The time in milliseconds to seek to.
*/
public void timeshiftSeekTo(long timeMs) {
- if (!TvCommonConstants.HAS_TIME_SHIFT_API) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Log.w(TAG, "Time shifting is not supported in this platform.");
return;
}
@@ -1216,7 +1237,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
* Returns the current playback position in milliseconds.
*/
public long timeshiftGetCurrentPositionMs() {
- if (!TvCommonConstants.HAS_TIME_SHIFT_API) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Log.w(TAG, "Time shifting is not supported in this platform.");
return INVALID_TIME;
}
@@ -1241,9 +1262,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
public abstract void onAvailabilityChanged();
/**
- * Called when the record start time has been changed..
+ * Called when the record start time has been changed.
*/
public abstract void onRecordStartTimeChanged(long recordStartTimeMs);
+
+ /**
+ * Called when the record end time has been changed.
+ */
+ public abstract void onRecordEndTimeChanged(long recordEndTimeMs);
}
/**
diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java
index 2bd4fcc5..124f3393 100644
--- a/src/com/android/tv/ui/TvOverlayManager.java
+++ b/src/com/android/tv/ui/TvOverlayManager.java
@@ -16,13 +16,26 @@
package com.android.tv.ui;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentManager.OnBackStackChangedListener;
+import android.content.Intent;
+import android.media.tv.TvInputInfo;
+import android.os.Build;
+import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
import android.util.Log;
+import android.view.Gravity;
import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
import android.view.ViewGroup;
+import android.widget.Space;
import com.android.tv.ApplicationSingletons;
import com.android.tv.ChannelTuner;
@@ -33,34 +46,44 @@ import com.android.tv.TimeShiftManager;
import com.android.tv.TvApplication;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.WeakHandler;
+import com.android.tv.common.ui.setup.OnActionClickListener;
+import com.android.tv.common.ui.setup.SetupFragment;
+import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
+import com.android.tv.data.ChannelDataManager;
import com.android.tv.dialog.FullscreenDialogFragment;
import com.android.tv.dialog.PinDialogFragment;
import com.android.tv.dialog.RecentlyWatchedDialogFragment;
import com.android.tv.dialog.SafeDismissDialogFragment;
+import com.android.tv.dvr.ui.DvrActivity;
import com.android.tv.guide.ProgramGuide;
import com.android.tv.menu.Menu;
import com.android.tv.menu.Menu.MenuShowReason;
import com.android.tv.menu.MenuRowFactory;
import com.android.tv.menu.MenuView;
+import com.android.tv.onboarding.NewSourcesFragment;
+import com.android.tv.onboarding.SetupSourcesFragment;
+import com.android.tv.onboarding.SetupSourcesFragment.InputSetupRunnable;
import com.android.tv.search.ProgramGuideSearchFragment;
import com.android.tv.ui.TvTransitionManager.SceneType;
-import com.android.tv.ui.sidepanel.AboutFragment;
+import com.android.tv.ui.sidepanel.SettingsFragment;
import com.android.tv.ui.sidepanel.SideFragmentManager;
import com.android.tv.ui.sidepanel.parentalcontrols.RatingsFragment;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
/**
* A class responsible for the life cycle and event handling of the pop-ups over TV view.
*/
// TODO: Put TvTransitionManager into this class.
+@UiThread
public class TvOverlayManager {
private static final String TAG = "TvOverlayManager";
private static final boolean DEBUG = false;
- public static final String SETUP_TRACKER_LABEL = "Setup dialog";
public static final String INTRO_TRACKER_LABEL = "Intro dialog";
@Retention(RetentionPolicy.SOURCE)
@@ -68,50 +91,54 @@ public class TvOverlayManager {
value = {FLAG_HIDE_OVERLAYS_DEFAULT, FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION,
FLAG_HIDE_OVERLAYS_KEEP_SCENE, FLAG_HIDE_OVERLAYS_KEEP_DIALOG,
FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS, FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY,
- FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE, FLAG_HIDE_OVERLAYS_KEEP_MENU})
+ FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE, FLAG_HIDE_OVERLAYS_KEEP_MENU,
+ FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT})
public @interface HideOverlayFlag {}
// FLAG_HIDE_OVERLAYs must be bitwise exclusive.
- public static final int FLAG_HIDE_OVERLAYS_DEFAULT = 0b00000000;
- public static final int FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION = 0b00000010;
- public static final int FLAG_HIDE_OVERLAYS_KEEP_SCENE = 0b00000100;
- public static final int FLAG_HIDE_OVERLAYS_KEEP_DIALOG = 0b00001000;
- public static final int FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS = 0b00010000;
- public static final int FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY = 0b00100000;
- public static final int FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE = 0b01000000;
- public static final int FLAG_HIDE_OVERLAYS_KEEP_MENU = 0b10000000;
-
- public static final int MSG_SHOW_DIALOG = 1000;
+ public static final int FLAG_HIDE_OVERLAYS_DEFAULT = 0b000000000;
+ public static final int FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION = 0b000000010;
+ public static final int FLAG_HIDE_OVERLAYS_KEEP_SCENE = 0b000000100;
+ public static final int FLAG_HIDE_OVERLAYS_KEEP_DIALOG = 0b000001000;
+ public static final int FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS = 0b000010000;
+ public static final int FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY = 0b000100000;
+ public static final int FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE = 0b001000000;
+ public static final int FLAG_HIDE_OVERLAYS_KEEP_MENU = 0b010000000;
+ public static final int FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT = 0b100000000;
+
+ public static final int MSG_OVERLAY_CLOSED = 1000;
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true,
value = {OVERLAY_TYPE_NONE, OVERLAY_TYPE_MENU, OVERLAY_TYPE_SIDE_FRAGMENT,
OVERLAY_TYPE_DIALOG, OVERLAY_TYPE_GUIDE, OVERLAY_TYPE_SCENE_CHANNEL_BANNER,
OVERLAY_TYPE_SCENE_INPUT_BANNER, OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH,
- OVERLAY_TYPE_SCENE_SELECT_INPUT})
+ OVERLAY_TYPE_SCENE_SELECT_INPUT, OVERLAY_TYPE_FRAGMENT})
private @interface TvOverlayType {}
// OVERLAY_TYPEs must be bitwise exclusive.
- private static final int OVERLAY_TYPE_NONE = 0b00000000;
- private static final int OVERLAY_TYPE_MENU = 0b00000001;
- private static final int OVERLAY_TYPE_SIDE_FRAGMENT = 0b00000010;
- private static final int OVERLAY_TYPE_DIALOG = 0b00000100;
- private static final int OVERLAY_TYPE_GUIDE = 0b00001000;
- private static final int OVERLAY_TYPE_SCENE_CHANNEL_BANNER = 0b00010000;
- private static final int OVERLAY_TYPE_SCENE_INPUT_BANNER = 0b00100000;
- private static final int OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH = 0b01000000;
- private static final int OVERLAY_TYPE_SCENE_SELECT_INPUT = 0b10000000;
+ private static final int OVERLAY_TYPE_NONE = 0b000000000;
+ private static final int OVERLAY_TYPE_MENU = 0b000000001;
+ private static final int OVERLAY_TYPE_SIDE_FRAGMENT = 0b000000010;
+ private static final int OVERLAY_TYPE_DIALOG = 0b000000100;
+ private static final int OVERLAY_TYPE_GUIDE = 0b000001000;
+ private static final int OVERLAY_TYPE_SCENE_CHANNEL_BANNER = 0b000010000;
+ private static final int OVERLAY_TYPE_SCENE_INPUT_BANNER = 0b000100000;
+ private static final int OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH = 0b001000000;
+ private static final int OVERLAY_TYPE_SCENE_SELECT_INPUT = 0b010000000;
+ private static final int OVERLAY_TYPE_FRAGMENT = 0b100000000;
private static final Set<String> AVAILABLE_DIALOG_TAGS = new HashSet<>();
static {
AVAILABLE_DIALOG_TAGS.add(RecentlyWatchedDialogFragment.DIALOG_TAG);
AVAILABLE_DIALOG_TAGS.add(PinDialogFragment.DIALOG_TAG);
AVAILABLE_DIALOG_TAGS.add(FullscreenDialogFragment.DIALOG_TAG);
- AVAILABLE_DIALOG_TAGS.add(AboutFragment.LicenseActionItem.DIALOG_TAG);
+ AVAILABLE_DIALOG_TAGS.add(SettingsFragment.LicenseActionItem.DIALOG_TAG);
AVAILABLE_DIALOG_TAGS.add(RatingsFragment.AttributionItem.DIALOG_TAG);
}
private final MainActivity mMainActivity;
private final ChannelTuner mChannelTuner;
private final TvTransitionManager mTransitionManager;
+ private final ChannelDataManager mChannelDataManager;
private final Menu mMenu;
private final SideFragmentManager mSideFragmentManager;
private final ProgramGuide mProgramGuide;
@@ -120,10 +147,16 @@ public class TvOverlayManager {
private final ProgramGuideSearchFragment mSearchFragment;
private final Tracker mTracker;
private SafeDismissDialogFragment mCurrentDialog;
+ private final SetupSourcesFragment mSetupFragment;
+ private boolean mSetupFragmentActive;
+ private final NewSourcesFragment mNewSourcesFragment;
+ private boolean mNewSourcesFragmentActive;
private final Handler mHandler = new TvOverlayHandler(this);
private @TvOverlayType int mOpenedOverlays;
+ private List<Runnable> mPendingActions = new ArrayList<>();
+
public TvOverlayManager(MainActivity mainActivity, ChannelTuner channelTuner,
KeypadChannelSwitchView keypadChannelSwitchView,
ChannelBannerView channelBannerView, InputBannerView inputBannerView,
@@ -131,10 +164,11 @@ public class TvOverlayManager {
ProgramGuideSearchFragment searchFragment) {
mMainActivity = mainActivity;
mChannelTuner = channelTuner;
+ ApplicationSingletons singletons = TvApplication.getSingletons(mainActivity);
+ mChannelDataManager = singletons.getChannelDataManager();
mKeypadChannelSwitchView = keypadChannelSwitchView;
mSelectInputView = selectInputView;
mSearchFragment = searchFragment;
- ApplicationSingletons singletons = TvApplication.getSingletons(mainActivity);
mTracker = singletons.getTracker();
mTransitionManager = new TvTransitionManager(mainActivity, sceneContainer,
channelBannerView, inputBannerView, mKeypadChannelSwitchView, selectInputView);
@@ -194,9 +228,46 @@ public class TvOverlayManager {
}
};
mProgramGuide = new ProgramGuide(mainActivity, channelTuner,
- singletons.getTvInputManagerHelper(), singletons.getChannelDataManager(),
+ singletons.getTvInputManagerHelper(), mChannelDataManager,
singletons.getProgramDataManager(), singletons.getTracker(), preShowRunnable,
postHideRunnable);
+ mSetupFragment = new SetupSourcesFragment();
+ mSetupFragment.setOnActionClickListener(new OnActionClickListener() {
+ @Override
+ public void onActionClick(String category, int id) {
+ switch (id) {
+ case SetupMultiPaneFragment.ACTION_DONE:
+ closeSetupFragment(true);
+ break;
+ case SetupSourcesFragment.ACTION_PLAY_STORE:
+ mMainActivity.showMerchantCollection();
+ break;
+ }
+ }
+ });
+ mSetupFragment.setInputSetupRunnable(new InputSetupRunnable() {
+ @Override
+ public void runInputSetup(TvInputInfo input) {
+ mMainActivity.startSetupActivity(input, true);
+ }
+ });
+ mNewSourcesFragment = new NewSourcesFragment();
+ mNewSourcesFragment.setOnActionClickListener(new OnActionClickListener() {
+ @Override
+ public void onActionClick(String category, int id) {
+ switch (id) {
+ case NewSourcesFragment.ACTION_SETUP:
+ closeNewSourcesFragment(false);
+ showSetupFragment();
+ break;
+ case NewSourcesFragment.ACTION_SKIP:
+ // Don't remove the fragment because new fragment will be replaced with
+ // this fragment.
+ closeNewSourcesFragment(true);
+ break;
+ }
+ }
+ });
}
/**
@@ -230,6 +301,20 @@ public class TvOverlayManager {
}
/**
+ * Checks whether the setup fragment is active or not.
+ */
+ public boolean isSetupFragmentActive() {
+ return mSetupFragmentActive;
+ }
+
+ /**
+ * Checks whether the new sources fragment is active or not.
+ */
+ public boolean isNewSourcesFragmentActive() {
+ return mNewSourcesFragmentActive;
+ }
+
+ /**
* Returns the instance of {@link ProgramGuide}.
*/
public ProgramGuide getProgramGuide() {
@@ -261,14 +346,6 @@ public class TvOverlayManager {
*/
public void showDialogFragment(String tag, SafeDismissDialogFragment dialog,
boolean keepSidePanelHistory) {
- showDialogFragment(tag, dialog, keepSidePanelHistory, 0);
- }
-
- /**
- * Shows the given dialog with a delay {@code delayMillis}.
- */
- public void showDialogFragment(String tag, SafeDismissDialogFragment dialog,
- boolean keepSidePanelHistory, long delayMillis) {
int flags = FLAG_HIDE_OVERLAYS_KEEP_DIALOG;
if (keepSidePanelHistory) {
flags |= FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY;
@@ -279,17 +356,14 @@ public class TvOverlayManager {
return;
}
- // TODO: Consider showing multiple dialog at once.
- if (mCurrentDialog != null && mCurrentDialog.isAdded()) {
+ Fragment old = mMainActivity.getFragmentManager().findFragmentByTag(tag);
+ // Do not show the dialog if the same kind of dialog is already opened.
+ if (old != null) {
return;
}
mCurrentDialog = dialog;
- if (delayMillis == 0) {
- dialog.show(mMainActivity.getFragmentManager(), tag);
- } else {
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SHOW_DIALOG, tag), delayMillis);
- }
+ dialog.show(mMainActivity.getFragmentManager(), tag);
// Calling this from SafeDismissDialogFragment.onCreated() might be late
// because it takes time for onCreated to be called
@@ -297,21 +371,101 @@ public class TvOverlayManager {
onOverlayOpened(OVERLAY_TYPE_DIALOG);
}
+ private void runAfterSideFragmentsAreClosed(final Runnable runnable) {
+ final FragmentManager manager = mMainActivity.getFragmentManager();
+ if (mSideFragmentManager.isSidePanelVisible()) {
+ manager.addOnBackStackChangedListener(new OnBackStackChangedListener() {
+ @Override
+ public void onBackStackChanged() {
+ if (manager.getBackStackEntryCount() == 0) {
+ manager.removeOnBackStackChangedListener(this);
+ runnable.run();
+ }
+ }
+ });
+ } else {
+ runnable.run();
+ }
+ }
+
+ private void showFragment(final Fragment fragment) {
+ hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
+ onOverlayOpened(OVERLAY_TYPE_FRAGMENT);
+ runAfterSideFragmentsAreClosed(new Runnable() {
+ @Override
+ public void run() {
+ mMainActivity.getFragmentManager().beginTransaction()
+ .replace(R.id.fragment_container, fragment).commit();
+ }
+ });
+ }
+
+ private void closeFragment(Fragment fragmentToRemove) {
+ onOverlayClosed(OVERLAY_TYPE_FRAGMENT);
+ if (fragmentToRemove != null) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ // In L, NPE happens if there is no next fragment when removing or hiding a fragment
+ // which has an exit transition. b/22631964
+ // A workaround is just replacing with a dummy fragment.
+ mMainActivity.getFragmentManager().beginTransaction()
+ .replace(R.id.fragment_container, new DummyFragment()).commit();
+ } else {
+ mMainActivity.getFragmentManager().beginTransaction().remove(fragmentToRemove)
+ .commit();
+ }
+ }
+ }
+
/**
* Shows setup dialog.
*/
- public void showSetupDialog() {
- showSetupDialog(0);
+ public void showSetupFragment() {
+ if (DEBUG) Log.d(TAG, "showSetupFragment");
+ mSetupFragmentActive = true;
+ SetupSourcesFragment.setTheme(R.style.Theme_TV_GuidedStep);
+ mSetupFragment.enableFragmentTransition(SetupFragment.FRAGMENT_ENTER_TRANSITION
+ | SetupFragment.FRAGMENT_EXIT_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION
+ | SetupFragment.FRAGMENT_REENTER_TRANSITION);
+ mSetupFragment.setFragmentTransition(SetupFragment.FRAGMENT_EXIT_TRANSITION, Gravity.END);
+ showFragment(mSetupFragment);
+ }
+
+ // Set removeFragment to false only when the new fragment is going to be shown.
+ private void closeSetupFragment(boolean removeFragment) {
+ if (DEBUG) Log.d(TAG, "closeSetupFragment");
+ if (!mSetupFragmentActive) {
+ return;
+ }
+ mSetupFragmentActive = false;
+ SetupSourcesFragment.setTheme(SetupSourcesFragment.DEFAULT_THEME);
+ closeFragment(removeFragment ? mSetupFragment : null);
+ if (mChannelDataManager.getChannelCount() == 0) {
+ mMainActivity.finish();
+ }
}
/**
- * Shows setup dialog with a delay {@code delayMillis}.
+ * Shows new sources dialog.
*/
- public void showSetupDialog(long delayMillis) {
- if (DEBUG) Log.d(TAG,"showSetupDialog");
- showDialogFragment(FullscreenDialogFragment.DIALOG_TAG,
- new FullscreenDialogFragment(R.layout.setup_dialog, SETUP_TRACKER_LABEL), false,
- delayMillis);
+ public void showNewSourcesFragment() {
+ if (DEBUG) Log.d(TAG, "showNewSourcesFragment");
+ mNewSourcesFragmentActive = true;
+ showFragment(mNewSourcesFragment);
+ }
+
+ // Set removeFragment to false only when the new fragment is going to be shown.
+ private void closeNewSourcesFragment(boolean removeFragment) {
+ if (DEBUG) Log.d(TAG, "closeNewSourcesFragment");
+ mNewSourcesFragmentActive = false;
+ closeFragment(removeFragment ? mNewSourcesFragment : null);
+ }
+
+ /**
+ * Shows DVR manager.
+ */
+ public void showDvrManager() {
+ Intent intent = new Intent(mMainActivity, DvrActivity.class);
+ mMainActivity.startActivity(intent);
}
/**
@@ -320,7 +474,8 @@ public class TvOverlayManager {
public void showIntroDialog() {
if (DEBUG) Log.d(TAG,"showIntroDialog");
showDialogFragment(FullscreenDialogFragment.DIALOG_TAG,
- new FullscreenDialogFragment(R.layout.intro_dialog, INTRO_TRACKER_LABEL), false);
+ FullscreenDialogFragment.newInstance(R.layout.intro_dialog, INTRO_TRACKER_LABEL),
+ false);
}
/**
@@ -341,7 +496,8 @@ public class TvOverlayManager {
public void showKeypadChannelSwitch() {
hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
- | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG);
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
mTransitionManager.goToKeypadChannelSwitchScene();
}
@@ -385,8 +541,8 @@ public class TvOverlayManager {
*/
// TODO: Add test for this method.
public void hideOverlays(@HideOverlayFlag int flags) {
- if (mMainActivity.needToKeepDialogWhenHidingOverlay()) {
- flags |= FLAG_HIDE_OVERLAYS_KEEP_DIALOG;
+ if (mMainActivity.needToKeepSetupScreenWhenHidingOverlay()) {
+ flags |= FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT;
}
if ((flags & FLAG_HIDE_OVERLAYS_KEEP_DIALOG) != 0) {
// Keeps the dialog.
@@ -398,18 +554,24 @@ public class TvOverlayManager {
// to null.
((PinDialogFragment) mCurrentDialog).setResultListener(null);
}
- if (mHandler.hasMessages(MSG_SHOW_DIALOG)) {
- mHandler.removeMessages(MSG_SHOW_DIALOG);
- onDialogDestroyed();
- } else {
- mCurrentDialog.dismiss();
- }
+ mCurrentDialog.dismiss();
}
mCurrentDialog = null;
}
-
boolean withAnimation = (flags & FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION) == 0;
+ if ((flags & FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT) == 0) {
+ if (mSetupFragmentActive) {
+ if (!withAnimation) {
+ mSetupFragment.setReturnTransition(null);
+ mSetupFragment.setExitTransition(null);
+ }
+ closeSetupFragment(true);
+ } else if (mNewSourcesFragmentActive) {
+ closeNewSourcesFragment(true);
+ }
+ }
+
if ((flags & FLAG_HIDE_OVERLAYS_KEEP_MENU) != 0) {
// Keeps the menu.
} else {
@@ -441,10 +603,12 @@ public class TvOverlayManager {
* UIs except banner is shown, the informational text needs to be hidden for clean UI.
*/
public boolean needHideTextOnMainView() {
- return getSideFragmentManager().isActive()
+ return mSideFragmentManager.isActive()
|| getMenu().isActive()
|| mTransitionManager.isKeypadChannelSwitchActive()
- || mTransitionManager.isSelectInputActive();
+ || mTransitionManager.isSelectInputActive()
+ || mSetupFragmentActive
+ || mNewSourcesFragmentActive;
}
@TvOverlayType private int convertSceneToOverlayType(@SceneType int sceneType) {
@@ -463,37 +627,61 @@ public class TvOverlayManager {
}
}
+ @UiThread
private void onOverlayOpened(@TvOverlayType int overlayType) {
if (DEBUG) Log.d(TAG, "Overlay opened: 0b" + Integer.toBinaryString(overlayType));
mOpenedOverlays |= overlayType;
if (DEBUG) Log.d(TAG, "Opened overlays: 0b" + Integer.toBinaryString(mOpenedOverlays));
+ mHandler.removeMessages(MSG_OVERLAY_CLOSED);
mMainActivity.updateKeyInputFocus();
}
+ @UiThread
private void onOverlayClosed(@TvOverlayType int overlayType) {
if (DEBUG) Log.d(TAG, "Overlay closed: 0b" + Integer.toBinaryString(overlayType));
mOpenedOverlays &= ~overlayType;
if (DEBUG) Log.d(TAG, "Opened overlays: 0b" + Integer.toBinaryString(mOpenedOverlays));
+ mHandler.removeMessages(MSG_OVERLAY_CLOSED);
mMainActivity.updateKeyInputFocus();
- boolean onlyBannerOrNoneOpened = (mOpenedOverlays & ~OVERLAY_TYPE_SCENE_CHANNEL_BANNER
- & ~OVERLAY_TYPE_SCENE_INPUT_BANNER) == 0;
// Show the main menu again if there are no pop-ups or banners only.
// The main menu should not be shown when the activity is in paused state.
- boolean wasMenuShown = false;
- if (mMainActivity.isActivityResumed() && onlyBannerOrNoneOpened) {
- wasMenuShown = showMenuWithTimeShiftPauseIfNeeded();
+ boolean menuAboutToShow = false;
+ if (canExecuteCloseAction()) {
+ menuAboutToShow = mMainActivity.getTimeShiftManager().isPaused();
+ mHandler.sendEmptyMessage(MSG_OVERLAY_CLOSED);
}
// Don't set screen name to main if the overlay closing is a banner
// or if a non banner overlay is still open
// or if we just opened the menu
if (overlayType != OVERLAY_TYPE_SCENE_CHANNEL_BANNER
&& overlayType != OVERLAY_TYPE_SCENE_INPUT_BANNER
- && onlyBannerOrNoneOpened
- && !wasMenuShown) {
+ && isOnlyBannerOrNoneOpened()
+ && !menuAboutToShow) {
mTracker.sendScreenView(MainActivity.SCREEN_NAME);
}
}
+ private boolean canExecuteCloseAction() {
+ return mMainActivity.isActivityResumed() && isOnlyBannerOrNoneOpened();
+ }
+
+ private boolean isOnlyBannerOrNoneOpened() {
+ return (mOpenedOverlays & ~OVERLAY_TYPE_SCENE_CHANNEL_BANNER
+ & ~OVERLAY_TYPE_SCENE_INPUT_BANNER) == 0;
+ }
+
+ /**
+ * Runs a given {@code action} after all the overlays are closed.
+ */
+ @UiThread
+ public void runAfterOverlaysAreClosed(Runnable action) {
+ if (canExecuteCloseAction()) {
+ action.run();
+ } else {
+ mPendingActions.add(action);
+ }
+ }
+
/**
* Handles the onUserInteraction event of the {@link MainActivity}.
*/
@@ -520,7 +708,8 @@ public class TvOverlayManager {
// Consumes the keys which may trigger system's default music player.
return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
}
- if (mMenu.isActive() || mSideFragmentManager.isActive() || mProgramGuide.isActive()) {
+ if (mMenu.isActive() || mSideFragmentManager.isActive() || mProgramGuide.isActive()
+ || mSetupFragmentActive || mNewSourcesFragmentActive) {
return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY;
}
if (mTransitionManager.isKeypadChannelSwitchActive()) {
@@ -547,7 +736,9 @@ public class TvOverlayManager {
|| mSideFragmentManager.isActive()
|| mSearchFragment.isVisible()
|| mTransitionManager.isKeypadChannelSwitchActive()
- || mTransitionManager.isSelectInputActive()) {
+ || mTransitionManager.isSelectInputActive()
+ || mSetupFragmentActive
+ || mNewSourcesFragmentActive) {
// Do not handle media key when any pop-ups which can handle keys are active.
return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
}
@@ -625,7 +816,8 @@ public class TvOverlayManager {
timeShiftManager.play();
}
hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
- | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG);
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
}
if (mMenu.isActive()) {
@@ -654,6 +846,20 @@ public class TvOverlayManager {
MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED
: MainActivity.KEY_EVENT_HANDLER_RESULT_NOT_HANDLED;
}
+ if (mSetupFragmentActive) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ closeSetupFragment(true);
+ return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
+ }
+ return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY;
+ }
+ if (mNewSourcesFragmentActive) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ closeNewSourcesFragment(true);
+ return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
+ }
+ return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY;
+ }
return MainActivity.KEY_EVENT_HANDLER_RESULT_PASSTHROUGH;
}
@@ -681,11 +887,34 @@ public class TvOverlayManager {
@Override
public void handleMessage(Message msg, @NonNull TvOverlayManager tvOverlayManager) {
- if (msg.what == MSG_SHOW_DIALOG) {
- String tag = (String) msg.obj;
- tvOverlayManager.mCurrentDialog
- .show(tvOverlayManager.mMainActivity.getFragmentManager(), tag);
+ switch (msg.what) {
+ case MSG_OVERLAY_CLOSED:
+ if (!tvOverlayManager.canExecuteCloseAction()) {
+ return;
+ }
+ if (tvOverlayManager.showMenuWithTimeShiftPauseIfNeeded()) {
+ return;
+ }
+ if (!tvOverlayManager.mPendingActions.isEmpty()) {
+ Runnable action = tvOverlayManager.mPendingActions.get(0);
+ tvOverlayManager.mPendingActions.remove(action);
+ action.run();
+ }
+ break;
}
}
}
+
+ /**
+ * Dummny class for the workaround of b/22631964. See {@link #closeFragment}.
+ */
+ public static class DummyFragment extends Fragment {
+ @Override
+ public @Nullable View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ final View v = new Space(inflater.getContext());
+ v.setVisibility(View.GONE);
+ return v;
+ }
+ }
}
diff --git a/src/com/android/tv/ui/sidepanel/AboutFragment.java b/src/com/android/tv/ui/sidepanel/AboutFragment.java
deleted file mode 100644
index ee83e21e..00000000
--- a/src/com/android/tv/ui/sidepanel/AboutFragment.java
+++ /dev/null
@@ -1,187 +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.ui.sidepanel;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.res.Resources;
-import android.os.Build;
-import android.provider.Settings;
-import android.view.View;
-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;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Shows version, optional license information and Analytics OptOut.
- */
-public class AboutFragment extends SideFragment {
- private static final String TRACKER_LABEL = "about";
-
- /**
- * Shows the application version name.
- */
- private static final class VersionItem extends Item {
- @Override
- protected int getResourceId() {
- return R.layout.option_item_simple;
- }
-
- @Override
- protected void onBind(View view) {
- super.onBind(view);
- TextView titleView = (TextView) view.findViewById(R.id.title);
- titleView.setText(R.string.about_menu_version);
- TextView descriptionView = (TextView) view.findViewById(R.id.description);
- descriptionView.setText(TvApplication.getVersionName());
- }
-
- @Override
- protected void onSelected() {
- }
- }
-
- /**
- * Opens a dialog showing open source licenses.
- */
- 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;
-
- public LicenseActionItem(MainActivity mainActivity) {
- super(mainActivity.getString(R.string.about_menu_licenses));
- mMainActivity = mainActivity;
- }
-
- @Override
- protected void onSelected() {
- WebDialogFragment dialog = WebDialogFragment.newInstance(LicenseUtils.LICENSE_FILE,
- mMainActivity.getString(R.string.dialog_title_licenses), TRACKER_LABEL);
- mMainActivity.getOverlayManager().showDialogFragment(DIALOG_TAG, dialog, false);
- }
- }
-
- /**
- * 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.getSingletons(context).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.sendAccessibilityText(
- 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);
- }
-
- @Override
- public String getTrackerLabel() {
- return TRACKER_LABEL;
- }
-
- @Override
- protected List<Item> getItemList() {
- List<Item> items = new ArrayList<>();
- items.add(new VersionItem());
- 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));
- }
- boolean developerOptionEnabled = Settings.Secure.getInt(getActivity().getContentResolver(),
- Settings.Global.DEVELOPMENT_SETTINGS_ENABLED , 0) != 0;
- if (Features.DEVELOPER_OPTION.isEnabled(getActivity()) && developerOptionEnabled
- && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- Resources res = getActivity().getResources();
- items.add(new ActionItem(res.getString(R.string.side_panel_title_developer)) {
- @Override
- protected void onSelected() {
- getMainActivity().getOverlayManager().getSideFragmentManager().show(
- new DeveloperFragment());
- }
- });
- }
- return items;
- }
-}
diff --git a/src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java b/src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java
deleted file mode 100644
index 7289034f..00000000
--- a/src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java
+++ /dev/null
@@ -1,104 +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.ui.sidepanel;
-
-import android.view.View;
-import android.widget.Toast;
-
-import com.android.tv.MainActivity;
-import com.android.tv.R;
-import com.android.tv.util.SetupUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class ChannelSourcesFragment extends SideFragment {
- private static final String TRACKER_LABEL = "channel sources";
-
- private static int ADDITIONAL_DELAY_TO_SHOW_SETUP_DIALOG_MILLIS = 50;
-
- private final long mCurrentChannelId;
-
- public ChannelSourcesFragment(long currentChannelId) {
- mCurrentChannelId = currentChannelId;
- }
-
- @Override
- protected String getTitle() {
- return getString(R.string.side_panel_title_channel_sources);
- }
-
- @Override
- public String getTrackerLabel() {
- return TRACKER_LABEL;
- }
-
- @Override
- protected List<Item> getItemList() {
- List<Item> items = new ArrayList<>();
- final Item customizeChannelListItem = new SubMenuItem(
- getString(R.string.channel_source_item_customize_channels),
- getString(R.string.channel_source_item_customize_channels_description),
- 0, getMainActivity().getOverlayManager().getSideFragmentManager()) {
- @Override
- protected SideFragment getFragment() {
- return new CustomizeChannelListFragment(mCurrentChannelId);
- }
-
- @Override
- protected void onBind(View view) {
- super.onBind(view);
- setEnabled(false);
- }
-
- @Override
- protected void onUpdate() {
- super.onUpdate();
- setEnabled(getChannelDataManager().getChannelCount() != 0);
- }
- };
- customizeChannelListItem.setEnabled(false);
- items.add(customizeChannelListItem);
- final MainActivity activity = getMainActivity();
- boolean hasNewInput = SetupUtils.getInstance(activity).hasNewInput(
- activity.getTvInputManagerHelper());
- items.add(new ActionItem(
- getString(R.string.channel_source_item_setup),
- hasNewInput ? getString(R.string.channel_source_item_setup_new_inputs)
- : null) {
- @Override
- protected void onSelected() {
- closeFragment();
- // Running two animations at the same time causes performance drop.
- // Show the setup dialog with delayed animation.
- activity.getOverlayManager().showSetupDialog(
- activity.getResources().getInteger(R.integer.side_panel_anim_short_duration)
- + ADDITIONAL_DELAY_TO_SHOW_SETUP_DIALOG_MILLIS);
- }
- });
- return items;
- }
-
- @Override
- public void onResume() {
- super.onResume();
- if (getChannelDataManager().areAllChannelsHidden()) {
- Toast.makeText(getActivity(), R.string.msg_all_channels_hidden, Toast.LENGTH_SHORT)
- .show();
- }
- }
-} \ No newline at end of file
diff --git a/src/com/android/tv/ui/sidepanel/DeveloperFragment.java b/src/com/android/tv/ui/sidepanel/DeveloperFragment.java
index 13f6c866..44b4d452 100644
--- a/src/com/android/tv/ui/sidepanel/DeveloperFragment.java
+++ b/src/com/android/tv/ui/sidepanel/DeveloperFragment.java
@@ -18,18 +18,10 @@ package com.android.tv.ui.sidepanel;
import android.app.Activity;
import android.content.Context;
-import android.content.res.Resources;
-import android.provider.Settings;
import android.view.View;
-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;
import java.util.ArrayList;
import java.util.List;
@@ -67,32 +59,6 @@ public class DeveloperFragment extends SideFragment {
}
}
- /**
- * Shows AC3 capability of the connected TV.
- */
- private static final class Ac3CapabilityItem extends Item {
- @Override
- protected int getResourceId() {
- return R.layout.option_item_simple;
- }
-
- @Override
- protected void onBind(View view) {
- super.onBind(view);
- TextView titleView = (TextView) view.findViewById(R.id.title);
- titleView.setText(R.string.developer_menu_ac3_support);
- TextView descriptionView = (TextView) view.findViewById(R.id.description);
- Resources res = view.getContext().getResources();
- boolean ac3Support = ((MainActivity) view.getContext()).isAc3PassthroughSupported();
- descriptionView.setText(ac3Support ? R.string.developer_menu_ac3_support_yes
- : R.string.developer_menu_ac3_support_no);
- }
-
- @Override
- protected void onSelected() {
- }
- }
-
@Override
protected String getTitle() {
return getResources().getString(R.string.side_panel_title_developer);
@@ -108,7 +74,11 @@ public class DeveloperFragment extends SideFragment {
List<Item> items = new ArrayList<>();
Activity activity = getActivity();
items.add(new UsbTvTunerItem(activity));
- items.add(new Ac3CapabilityItem());
+ boolean ac3Support = getMainActivity().isAc3PassthroughSupported();
+ // Show AC3 passthrough availability.
+ items.add(new SimpleItem(getString(R.string.developer_menu_ac3_support),
+ getString(ac3Support ? R.string.developer_menu_ac3_support_yes
+ : R.string.developer_menu_ac3_support_no)));
return items;
}
}
diff --git a/src/com/android/tv/ui/sidepanel/Item.java b/src/com/android/tv/ui/sidepanel/Item.java
index 4ae6e523..00f16427 100644
--- a/src/com/android/tv/ui/sidepanel/Item.java
+++ b/src/com/android/tv/ui/sidepanel/Item.java
@@ -16,9 +16,11 @@
package com.android.tv.ui.sidepanel;
+import android.support.annotation.UiThread;
import android.view.View;
import android.view.ViewGroup;
+@UiThread
public abstract class Item {
private View mItemView;
private boolean mEnabled = true;
diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
new file mode 100644
index 00000000..6b5b2584
--- /dev/null
+++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
@@ -0,0 +1,185 @@
+/*
+ * 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.ui.sidepanel;
+
+import android.content.res.Resources;
+import android.os.Build;
+import android.provider.Settings;
+import android.view.View;
+import android.widget.Toast;
+
+import com.android.tv.Features;
+import com.android.tv.MainActivity;
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dialog.PinDialogFragment;
+import com.android.tv.dialog.WebDialogFragment;
+import com.android.tv.license.LicenseUtils;
+import com.android.tv.ui.sidepanel.parentalcontrols.ParentalControlsFragment;
+import com.android.tv.util.PermissionUtils;
+import com.android.tv.util.SetupUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Shows Live TV settings.
+ */
+public class SettingsFragment extends SideFragment {
+ private static final String TRACKER_LABEL = "settings";
+
+ private final long mCurrentChannelId;
+
+ public SettingsFragment(long currentChannelId) {
+ mCurrentChannelId = currentChannelId;
+ }
+
+ /**
+ * Opens a dialog showing open source licenses.
+ */
+ 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;
+
+ public LicenseActionItem(MainActivity mainActivity) {
+ super(mainActivity.getString(R.string.settings_menu_licenses));
+ mMainActivity = mainActivity;
+ }
+
+ @Override
+ protected void onSelected() {
+ WebDialogFragment dialog = WebDialogFragment.newInstance(LicenseUtils.LICENSE_FILE,
+ mMainActivity.getString(R.string.dialog_title_licenses), TRACKER_LABEL);
+ mMainActivity.getOverlayManager().showDialogFragment(DIALOG_TAG, dialog, false);
+ }
+ }
+
+ @Override
+ protected String getTitle() {
+ return getResources().getString(R.string.side_panel_title_settings);
+ }
+
+ @Override
+ public String getTrackerLabel() {
+ return TRACKER_LABEL;
+ }
+
+ @Override
+ protected List<Item> getItemList() {
+ List<Item> items = new ArrayList<>();
+ final Item customizeChannelListItem = new SubMenuItem(
+ getString(R.string.settings_channel_source_item_customize_channels),
+ getString(R.string.settings_channel_source_item_customize_channels_description),
+ 0, getMainActivity().getOverlayManager().getSideFragmentManager()) {
+ @Override
+ protected SideFragment getFragment() {
+ return new CustomizeChannelListFragment(mCurrentChannelId);
+ }
+
+ @Override
+ protected void onBind(View view) {
+ super.onBind(view);
+ setEnabled(false);
+ }
+
+ @Override
+ protected void onUpdate() {
+ super.onUpdate();
+ setEnabled(getChannelDataManager().getChannelCount() != 0);
+ }
+ };
+ customizeChannelListItem.setEnabled(false);
+ items.add(customizeChannelListItem);
+ final MainActivity activity = getMainActivity();
+ boolean hasNewInput = SetupUtils.getInstance(activity).hasNewInput(
+ activity.getTvInputManagerHelper());
+ items.add(new ActionItem(
+ getString(R.string.settings_channel_source_item_setup),
+ hasNewInput ? getString(R.string.settings_channel_source_item_setup_new_inputs)
+ : null) {
+ @Override
+ protected void onSelected() {
+ closeFragment();
+ activity.getOverlayManager().showSetupFragment();
+ }
+ });
+ if (PermissionUtils.hasModifyParentalControls(getMainActivity())) {
+ items.add(new ActionItem(getString(R.string.settings_parental_controls),
+ getString(activity.getParentalControlSettings().isParentalControlsEnabled()
+ ? R.string.option_toggle_parental_controls_on
+ : R.string.option_toggle_parental_controls_off)) {
+ @Override
+ protected void onSelected() {
+ final MainActivity tvActivity = getMainActivity();
+ final SideFragmentManager sideFragmentManager = tvActivity.getOverlayManager()
+ .getSideFragmentManager();
+ sideFragmentManager.hideSidePanel(true);
+ PinDialogFragment fragment = new PinDialogFragment(
+ PinDialogFragment.PIN_DIALOG_TYPE_ENTER_PIN,
+ new PinDialogFragment.ResultListener() {
+ @Override
+ public void done(boolean success) {
+ if (success) {
+ sideFragmentManager.show(new ParentalControlsFragment(),
+ false);
+ sideFragmentManager.showSidePanel(true);
+ } else {
+ sideFragmentManager.hideAll(false);
+ }
+ }
+ });
+ tvActivity.getOverlayManager().showDialogFragment(PinDialogFragment.DIALOG_TAG,
+ fragment, true);
+ }
+ });
+ } 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.
+ }
+ if (LicenseUtils.hasLicenses(activity.getAssets())) {
+ items.add(new LicenseActionItem(activity));
+ }
+ boolean developerOptionEnabled = Settings.Secure.getInt(getActivity().getContentResolver(),
+ Settings.Global.DEVELOPMENT_SETTINGS_ENABLED , 0) != 0;
+ if (Features.DEVELOPER_OPTION.isEnabled(getActivity()) && developerOptionEnabled
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ Resources res = getActivity().getResources();
+ items.add(new ActionItem(res.getString(R.string.side_panel_title_developer)) {
+ @Override
+ protected void onSelected() {
+ getMainActivity().getOverlayManager().getSideFragmentManager().show(
+ new DeveloperFragment());
+ }
+ });
+ }
+ // Show version.
+ items.add(new SimpleItem(getString(R.string.settings_menu_version),
+ ((TvApplication) activity.getApplicationContext()).getVersionName()));
+ return items;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (getChannelDataManager().areAllChannelsHidden()) {
+ Toast.makeText(getActivity(), R.string.msg_all_channels_hidden, Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+}
diff --git a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java
index b1041629..faccbc66 100644
--- a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java
+++ b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java
@@ -86,7 +86,17 @@ public class SideFragmentManager {
return mHideAnimator.isStarted();
}
+ /**
+ * Shows the given {@link SideFragment}.
+ */
public void show(SideFragment sideFragment) {
+ show(sideFragment, true);
+ }
+
+ /**
+ * Shows the given {@link SideFragment}.
+ */
+ public void show(SideFragment sideFragment, boolean showEnterAnimation) {
SideFragment.preloadRecycledViews(mActivity);
if (isHiding()) {
mHideAnimator.end();
@@ -101,7 +111,7 @@ public class SideFragmentManager {
FragmentTransaction ft = mFragmentManager.beginTransaction();
if (!isFirst) {
ft.setCustomAnimations(
- R.animator.side_panel_fragment_enter,
+ showEnterAnimation ? R.animator.side_panel_fragment_enter : 0,
R.animator.side_panel_fragment_exit,
R.animator.side_panel_fragment_pop_enter,
R.animator.side_panel_fragment_pop_exit);
diff --git a/src/com/android/tv/ui/sidepanel/SimpleItem.java b/src/com/android/tv/ui/sidepanel/SimpleItem.java
new file mode 100644
index 00000000..52a5f13f
--- /dev/null
+++ b/src/com/android/tv/ui/sidepanel/SimpleItem.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.ui.sidepanel;
+
+/**
+ * A simple item which shows title and description.
+ */
+public class SimpleItem extends ActionItem {
+ public SimpleItem(String title) {
+ super(title);
+ }
+
+ public SimpleItem(String title, String description) {
+ super(title, description);
+ }
+
+ @Override
+ protected void onSelected() {
+ }
+}
diff --git a/src/com/android/tv/util/BitmapUtils.java b/src/com/android/tv/util/BitmapUtils.java
index fd07507a..78b77e65 100644
--- a/src/com/android/tv/util/BitmapUtils.java
+++ b/src/com/android/tv/util/BitmapUtils.java
@@ -21,7 +21,6 @@ import android.content.Context;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
@@ -37,7 +36,7 @@ import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
-public class BitmapUtils {
+public final class BitmapUtils {
private static final String TAG = "BitmapUtils";
private static final boolean DEBUG = false;
@@ -52,12 +51,8 @@ public class BitmapUtils {
private BitmapUtils() { /* cannot be instantiated */ }
public static Bitmap scaleBitmap(Bitmap bm, int maxWidth, int maxHeight) {
- Bitmap result;
Rect rect = calculateNewSize(bm, maxWidth, maxHeight);
- result = Bitmap.createBitmap(rect.right, rect.bottom, bm.getConfig());
- Canvas canvas = new Canvas(result);
- canvas.drawBitmap(bm, null, rect, null);
- return result;
+ return Bitmap.createScaledBitmap(bm, rect.right, rect.bottom, false);
}
private static Rect calculateNewSize(Bitmap bm, int maxWidth, int maxHeight) {
diff --git a/src/com/android/tv/util/CollectionUtils.java b/src/com/android/tv/util/CollectionUtils.java
deleted file mode 100644
index d1c50392..00000000
--- a/src/com/android/tv/util/CollectionUtils.java
+++ /dev/null
@@ -1,41 +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.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<>();
- } else {
- return new HashSet<>();
- }
- }
-}
diff --git a/src/com/android/tv/util/EngOnlyFeature.java b/src/com/android/tv/util/EngOnlyFeature.java
deleted file mode 100644
index f4cbe9cf..00000000
--- a/src/com/android/tv/util/EngOnlyFeature.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.tv.util;
-
-import android.content.Context;
-
-import com.android.tv.BuildConfig;
-import com.android.tv.common.feature.Feature;
-
-/**
- * A feature that is only available on {@link BuildConfig#ENG} builds.
- */
-public final class EngOnlyFeature implements Feature {
- public static Feature ENG_ONLY_FEATURE = new EngOnlyFeature();
-
- private EngOnlyFeature() { }
-
- @Override
- public boolean isEnabled(Context context) {
- return BuildConfig.ENG;
- }
-
- @Override
- public String toString() {
- return "EngOnlyFeature";
- }
-}
diff --git a/src/com/android/tv/util/ImageCache.java b/src/com/android/tv/util/ImageCache.java
index e849da89..db64d4c9 100644
--- a/src/com/android/tv/util/ImageCache.java
+++ b/src/com/android/tv/util/ImageCache.java
@@ -20,6 +20,7 @@ import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.util.LruCache;
+import com.android.tv.common.MemoryManageable;
import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
/**
diff --git a/src/com/android/tv/util/ImageLoader.java b/src/com/android/tv/util/ImageLoader.java
index 8e901dd0..59c4983b 100644
--- a/src/com/android/tv/util/ImageLoader.java
+++ b/src/com/android/tv/util/ImageLoader.java
@@ -22,18 +22,22 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.tv.TvInputInfo;
import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.annotation.WorkerThread;
import android.util.Log;
import com.android.tv.R;
+import com.android.tv.common.CollectionUtils;
import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
-import java.util.ArrayList;
+import java.lang.ref.WeakReference;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
@@ -45,15 +49,46 @@ public final class ImageLoader {
private static final String TAG = "ImageLoader";
private static final boolean DEBUG = false;
+ private static Handler sMainHandler;
+
/**
- * Interface definition for a callback to be invoked when image loading is finished.
+ * Handles when image loading is finished.
+ *
+ * <p>Use this to prevent leaking an Activity or other Context while image loading is
+ * still pending. When you extend this class you <strong>MUST NOT</strong> use a non static
+ * inner class, or the containing object will still be leaked.
*/
@UiThread
- public interface ImageLoaderCallback {
+ public static abstract class ImageLoaderCallback<T> {
+ private final WeakReference<T> mWeakReference;
+
+ /**
+ * Creates an callback keeping a weak reference to {@code referent}.
+ *
+ * <p> If the "referent" is no longer valid, it no longer makes sense to run the
+ * callback. The referent is the View, or Activity or whatever that actually needs to
+ * receive the Bitmap. If the referent has been GC, then no need to run the callback.
+ */
+ public ImageLoaderCallback(T referent) {
+ mWeakReference = new WeakReference<>(referent);
+ }
+
/**
* Called when bitmap is loaded.
*/
- void onBitmapLoaded(@Nullable Bitmap bitmap);
+ private void onBitmapLoaded(@Nullable Bitmap bitmap) {
+ T referent = mWeakReference.get();
+ if (referent != null) {
+ onBitmapLoaded(referent, bitmap);
+ } else {
+ if (DEBUG) Log.d(TAG, "onBitmapLoaded not called because weak reference is gone");
+ }
+ }
+
+ /**
+ * Called when bitmap is loaded if the weak reference is still valid.
+ */
+ public abstract void onBitmapLoaded(T referent, @Nullable Bitmap bitmap);
}
private static final Map<String, LoadBitmapTask> sPendingListMap = new HashMap<>();
@@ -62,14 +97,26 @@ public final class ImageLoader {
* Preload a bitmap image into the cache.
*
* <p>Not to make heavy CPU load, AsyncTask.SERIAL_EXECUTOR is used for the image loading.
+ * <p>This method is thread safe.
*/
- @UiThread
- public static void prefetchBitmap(Context context, String uriString,
- int maxWidth, int maxHeight) {
- if (DEBUG) {
- Log.d(TAG, "prefetchBitmap() " + uriString);
+ public static void prefetchBitmap(Context context, final String uriString, final int maxWidth,
+ final int maxHeight) {
+ if (DEBUG) Log.d(TAG, "prefetchBitmap() " + uriString);
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ doLoadBitmap(context, uriString, maxWidth, maxHeight, null, AsyncTask.SERIAL_EXECUTOR);
+ } else {
+ final Context appContext = context.getApplicationContext();
+ getMainHandler().post(new Runnable() {
+ @Override
+ @MainThread
+ public void run() {
+ // Calling from the main thread prevents a ConcurrentModificationException
+ // in LoadBitmapTask.onPostExecute
+ doLoadBitmap(appContext, uriString, maxWidth, maxHeight, null,
+ AsyncTask.SERIAL_EXECUTOR);
+ }
+ });
}
- doLoadBitmap(context, uriString, maxWidth, maxHeight, null, AsyncTask.SERIAL_EXECUTOR);
}
/**
@@ -138,6 +185,7 @@ public final class ImageLoader {
/**
* @return {@code true} if the load is complete and the callback is executed.
*/
+ @UiThread
private static boolean doLoadBitmap(ImageLoaderCallback callback, Executor executor,
LoadBitmapTask loadBitmapTask) {
ScaledBitmapInfo bitmapInfo = loadBitmapTask.getFromCache();
@@ -149,7 +197,7 @@ public final class ImageLoader {
return true;
}
LoadBitmapTask existingTask = sPendingListMap.get(loadBitmapTask.getKey());
- if (existingTask != null && !loadBitmapTask.isReloadNeeded(existingTask) ) {
+ if (existingTask != null && !loadBitmapTask.isReloadNeeded(existingTask)) {
// The image loading is already scheduled and is large enough.
if (callback != null) {
existingTask.mCallbacks.add(callback);
@@ -169,15 +217,16 @@ public final class ImageLoader {
return false;
}
-/**
- * Loads and caches a a possibly scaled down version of a bitmap.
- *
- * <p>Implement {@link #doGetBitmapInBackground()} to to the actual loading.
- */
+ /**
+ * Loads and caches a a possibly scaled down version of a bitmap.
+ *
+ * <p>Implement {@link #doGetBitmapInBackground} to do the actual loading.
+ */
public static abstract class LoadBitmapTask extends AsyncTask<Void, Void, ScaledBitmapInfo> {
+ protected final Context mAppContext;
protected final int mMaxWidth;
protected final int mMaxHeight;
- private final List<ImageLoader.ImageLoaderCallback> mCallbacks = new ArrayList<>();
+ private final Set<ImageLoaderCallback> mCallbacks = CollectionUtils.createSmallSet();
private final ImageCache mImageCache;
private final String mKey;
@@ -191,11 +240,12 @@ public final class ImageLoader {
.needToReload(mMaxWidth, mMaxHeight);
if (DEBUG) {
if (needToReload) {
- Log.d(TAG, "Bitmap needs to be reloaded. {originalWidth="
- + bitmapInfo.bitmap.getWidth() + ", originalHeight="
- + bitmapInfo.bitmap.getHeight() + ", reqWidth=" + mMaxWidth
- + ", reqHeight="
- + mMaxHeight);
+ Log.d(TAG, "Bitmap needs to be reloaded. {"
+ + "originalWidth=" + bitmapInfo.bitmap.getWidth()
+ + ", originalHeight=" + bitmapInfo.bitmap.getHeight()
+ + ", reqWidth=" + mMaxWidth
+ + ", reqHeight=" + mMaxHeight
+ + "}");
}
}
return needToReload;
@@ -213,11 +263,14 @@ public final class ImageLoader {
return mImageCache.get(mKey);
}
- public LoadBitmapTask(ImageCache imageCache, String key, int maxHeight, int maxWidth) {
+ public LoadBitmapTask(Context context, ImageCache imageCache, String key, int maxHeight,
+ int maxWidth) {
if (maxWidth == 0 || maxHeight == 0) {
- throw new IllegalArgumentException("Image size should not be 0. {width=" + maxWidth
- + ", height=" + maxHeight + "}");
+ throw new IllegalArgumentException(
+ "Image size should not be 0. {width=" + maxWidth + ", height=" + maxHeight
+ + "}");
}
+ mAppContext = context.getApplicationContext();
mKey = key;
mImageCache = imageCache;
mMaxHeight = maxHeight;
@@ -247,9 +300,8 @@ public final class ImageLoader {
@Override
public final void onPostExecute(ScaledBitmapInfo scaledBitmapInfo) {
- if (ImageLoader.DEBUG) {
- Log.d(ImageLoader.TAG, "Bitmap is loaded " + mKey);
- }
+ if (DEBUG) Log.d(ImageLoader.TAG, "Bitmap is loaded " + mKey);
+
for (ImageLoader.ImageLoaderCallback callback : mCallbacks) {
callback.onBitmapLoaded(scaledBitmapInfo == null ? null : scaledBitmapInfo.bitmap);
}
@@ -262,24 +314,22 @@ public final class ImageLoader {
@Override
public String toString() {
- return this.getClass().getSimpleName() + "(" + mKey + " "
- + mMaxWidth + "x" + mMaxHeight + ")";
+ return this.getClass().getSimpleName() + "(" + mKey + " " + mMaxWidth + "x" + mMaxHeight
+ + ")";
}
}
private static final class LoadBitmapFromUriTask extends LoadBitmapTask {
- private final Context mContext;
private LoadBitmapFromUriTask(Context context, ImageCache imageCache, String uriString,
int maxWidth, int maxHeight) {
- super(imageCache, uriString, maxHeight, maxWidth);
- mContext = context;
+ super(context, imageCache, uriString, maxHeight, maxWidth);
}
@Override
@Nullable
public final ScaledBitmapInfo doGetBitmapInBackground() {
return BitmapUtils
- .decodeSampledBitmapFromUriString(mContext, getKey(), mMaxWidth, mMaxHeight);
+ .decodeSampledBitmapFromUriString(mAppContext, getKey(), mMaxWidth, mMaxHeight);
}
}
@@ -288,10 +338,10 @@ public final class ImageLoader {
*/
public static final class LoadTvInputLogoTask extends LoadBitmapTask {
private final TvInputInfo mInfo;
- private final Context mContext;
public LoadTvInputLogoTask(Context context, ImageCache cache, TvInputInfo info) {
- super(cache,
+ super(context,
+ cache,
info.getId() + "-logo",
context.getResources()
.getDimensionPixelSize(R.dimen.channel_banner_input_logo_size),
@@ -299,13 +349,12 @@ public final class ImageLoader {
.getDimensionPixelSize(R.dimen.channel_banner_input_logo_size)
);
mInfo = info;
- mContext = context;
}
@Nullable
@Override
public ScaledBitmapInfo doGetBitmapInBackground() {
- Drawable drawable = mInfo.loadIcon(mContext);
+ Drawable drawable = mInfo.loadIcon(mAppContext);
if (!(drawable instanceof BitmapDrawable)) {
return null;
}
@@ -317,6 +366,13 @@ public final class ImageLoader {
}
}
+ private static synchronized Handler getMainHandler() {
+ if (sMainHandler == null) {
+ sMainHandler = new Handler(Looper.getMainLooper());
+ }
+ return sMainHandler;
+ }
+
private ImageLoader() {
}
}
diff --git a/src/com/android/tv/util/MultiLongSparseArray.java b/src/com/android/tv/util/MultiLongSparseArray.java
index 4c067892..7ed72d61 100644
--- a/src/com/android/tv/util/MultiLongSparseArray.java
+++ b/src/com/android/tv/util/MultiLongSparseArray.java
@@ -19,6 +19,8 @@ package com.android.tv.util;
import android.support.annotation.VisibleForTesting;
import android.util.LongSparseArray;
+import com.android.tv.common.CollectionUtils;
+
import java.util.Collections;
import java.util.Set;
diff --git a/src/com/android/tv/util/OnboardingUtils.java b/src/com/android/tv/util/OnboardingUtils.java
index 0570d590..582a0c9f 100644
--- a/src/com/android/tv/util/OnboardingUtils.java
+++ b/src/com/android/tv/util/OnboardingUtils.java
@@ -18,8 +18,10 @@ package com.android.tv.util;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.Intent;
import android.database.Cursor;
import android.media.tv.TvContract.Channels;
+import android.net.Uri;
import android.preference.PreferenceManager;
import android.support.annotation.UiThread;
@@ -31,7 +33,16 @@ import com.android.tv.data.ChannelDataManager;
*/
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_ONBOARDING_VERSION_CODE = "pref_onbaording_versionCode";
+ private static final int ONBOARDING_VERSION = 1;
+
+ private static final String MERCHANT_COLLECTION_URL_STRING =
+ "https://play.google.com/store/apps/collection/promotion_3001bf9_ATV_livechannels";
+ /**
+ * Intent to show merchant collection in play store.
+ */
+ public static final Intent PLAY_STORE_INTENT = new Intent(Intent.ACTION_VIEW,
+ Uri.parse(MERCHANT_COLLECTION_URL_STRING));
/**
* Checks if this is the first boot after the onboarding experience has been applied.
@@ -52,29 +63,29 @@ public final class OnboardingUtils {
}
/**
- * Checks if this is the first run of {@link com.android.tv.MainActivity} after the
- * onboarding experience has been applied.
+ * Checks if this is the first run of {@link com.android.tv.MainActivity} with the
+ * current onboarding version.
*/
- public static boolean isFirstRun(Context context) {
- return PreferenceManager.getDefaultSharedPreferences(context)
- .getBoolean(PREF_KEY_IS_FIRST_RUN, true);
+ public static boolean isFirstRunWithCurrentVersion(Context context) {
+ int versionCode = PreferenceManager.getDefaultSharedPreferences(context)
+ .getInt(PREF_KEY_ONBOARDING_VERSION_CODE, 0);
+ return versionCode != ONBOARDING_VERSION;
}
/**
- * Marks that the first run of {@link com.android.tv.MainActivity} has been completed.
+ * Marks that the first run of {@link com.android.tv.MainActivity} with the current
+ * onboarding version has been completed.
*/
- public static void setFirstRunCompleted(Context context) {
- PreferenceManager.getDefaultSharedPreferences(context)
- .edit()
- .putBoolean(PREF_KEY_IS_FIRST_RUN, false)
- .apply();
+ public static void setFirstRunWithCurrentVersionCompleted(Context context) {
+ PreferenceManager.getDefaultSharedPreferences(context).edit()
+ .putInt(PREF_KEY_ONBOARDING_VERSION_CODE, ONBOARDING_VERSION).apply();
}
/**
* Checks whether the onboarding screen should be shown or not.
*/
public static boolean needToShowOnboarding(Context context) {
- return isFirstRun(context) || !areChannelsAvailable(context);
+ return isFirstRunWithCurrentVersion(context) || !areChannelsAvailable(context);
}
/**
@@ -93,4 +104,12 @@ public final class OnboardingUtils {
return c.getCount() != 0;
}
}
+
+ /**
+ * Checks if there are any available TV inputs.
+ */
+ public static boolean areInputsAvailable(Context context) {
+ return TvApplication.getSingletons(context).getTvInputManagerHelper()
+ .getTvInputInfos(true, false).size() > 0;
+ }
}
diff --git a/src/com/android/tv/util/PipInputManager.java b/src/com/android/tv/util/PipInputManager.java
index 3e4db654..dddc82b6 100644
--- a/src/com/android/tv/util/PipInputManager.java
+++ b/src/com/android/tv/util/PipInputManager.java
@@ -24,6 +24,7 @@ import android.util.Log;
import com.android.tv.ChannelTuner;
import com.android.tv.R;
+import com.android.tv.common.CollectionUtils;
import com.android.tv.data.Channel;
import java.util.ArrayList;
diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java
index dcede666..2a006f9e 100644
--- a/src/com/android/tv/util/RecurringRunner.java
+++ b/src/com/android/tv/util/RecurringRunner.java
@@ -23,6 +23,8 @@ import android.os.Handler;
import android.support.annotation.WorkerThread;
import android.util.Log;
+import com.android.tv.common.SharedPreferencesUtils;
+
import java.util.Date;
/**
@@ -38,13 +40,16 @@ public final class RecurringRunner {
private final Handler mHandler;
private final long mIntervalMs;
private final Runnable mRunnable;
+ private final Runnable mOnStopRunnable;
private final Context mContext;
private final String mName;
private boolean mRunning;
- public RecurringRunner(Context context, long intervalMs, Runnable runnable) {
+ public RecurringRunner(Context context, long intervalMs, Runnable runnable,
+ Runnable onStopRunnable) {
mContext = context.getApplicationContext();
mRunnable = runnable;
+ mOnStopRunnable = onStopRunnable;
mIntervalMs = intervalMs;
if (DEBUG) Log.i(TAG, "Delaying " + (intervalMs / 1000.0) + " seconds");
mName = runnable.getClass().getCanonicalName();
@@ -73,6 +78,9 @@ public final class RecurringRunner {
public void stop() {
mRunning = false;
mHandler.removeCallbacksAndMessages(null);
+ if (mOnStopRunnable != null) {
+ mOnStopRunnable.run();
+ }
}
private void postAt(long next) {
@@ -102,7 +110,7 @@ public final class RecurringRunner {
}
private SharedPreferences getSharedPreferences() {
- return mContext.getSharedPreferences(RecurringRunner.class.getCanonicalName(),
+ return mContext.getSharedPreferences(SharedPreferencesUtils.SHARED_PREF_RECURRING_RUNNER,
Context.MODE_PRIVATE);
}
diff --git a/src/com/android/tv/util/SearchManagerHelper.java b/src/com/android/tv/util/SearchManagerHelper.java
index bd5db6ec..5ec1b455 100644
--- a/src/com/android/tv/util/SearchManagerHelper.java
+++ b/src/com/android/tv/util/SearchManagerHelper.java
@@ -52,7 +52,7 @@ public final class SearchManagerHelper {
public void launchAssistAction() {
try {
- if (Build.VERSION.SDK_INT >= 23) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
SearchManager.class.getDeclaredMethod(
"launchLegacyAssist", String.class, Integer.TYPE, Bundle.class).invoke(
mSearchManager, null, UserHandle.myUserId(), null);
diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java
index 0fbbce1e..d337139b 100644
--- a/src/com/android/tv/util/SetupUtils.java
+++ b/src/com/android/tv/util/SetupUtils.java
@@ -25,13 +25,17 @@ import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.os.Build;
import android.preference.PreferenceManager;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
import android.util.Log;
import com.android.tv.ApplicationSingletons;
import com.android.tv.TvApplication;
+import com.android.tv.common.CollectionUtils;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
+import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@@ -47,6 +51,8 @@ public class SetupUtils {
private static final String PREF_KEY_KNOWN_INPUTS = "known_inputs";
// 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";
+ // Recognized inputs means that the user already knows the inputs are installed.
+ private static final String PREF_KEY_RECOGNIZED_INPUTS = "recognized_inputs";
private static final String PREF_KEY_IS_FIRST_TUNE = "is_first_tune";
private static SetupUtils sSetupUtils;
@@ -54,16 +60,22 @@ public class SetupUtils {
private final SharedPreferences mSharedPreferences;
private final Set<String> mKnownInputs;
private final Set<String> mSetUpInputs;
+ private final Set<String> mRecognizedInputs;
private boolean mIsFirstTune;
private final String mUsbTunerInputId;
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,
- new HashSet<String>()));
+ mSetUpInputs = CollectionUtils.createSmallSet();
+ mSetUpInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_SET_UP_INPUTS,
+ Collections.<String>emptySet()));
+ mKnownInputs = CollectionUtils.createSmallSet();
+ mKnownInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_KNOWN_INPUTS,
+ Collections.<String>emptySet()));
+ mRecognizedInputs = CollectionUtils.createSmallSet();
+ mRecognizedInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS,
+ mKnownInputs));
mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true);
mUsbTunerInputId = TvContract.buildInputId(new ComponentName(tvApplication,
com.android.usbtuner.tvinput.UsbTunerTvInputService.class));
@@ -83,7 +95,8 @@ public class SetupUtils {
/**
* Additional work after the setup of TV input.
*/
- public void onTvInputSetupFinished(final String inputId, final Runnable postRunnable) {
+ public void onTvInputSetupFinished(final String inputId,
+ @Nullable 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
@@ -136,6 +149,37 @@ public class SetupUtils {
});
}
+ /**
+ * Marks the channels in newly installed inputs browsable.
+ */
+ @UiThread
+ public void markNewChannelsBrowsable() {
+ Set<String> newInputsWithChannels = new HashSet<>();
+ TvInputManagerHelper tvInputManagerHelper = mTvApplication.getTvInputManagerHelper();
+ ChannelDataManager channelDataManager = mTvApplication.getChannelDataManager();
+ SoftPreconditions.checkState(channelDataManager.isDbLoadFinished());
+ for (TvInputInfo input : tvInputManagerHelper.getTvInputInfos(true, true)) {
+ String inputId = input.getId();
+ if (!isSetupDone(inputId) && channelDataManager.getChannelCountForInput(inputId) > 0) {
+ onSetupDone(inputId);
+ newInputsWithChannels.add(inputId);
+ if (DEBUG) {
+ Log.d(TAG, "New input " + inputId + " has "
+ + channelDataManager.getChannelCountForInput(inputId)
+ + " channels");
+ }
+ }
+ }
+ if (!newInputsWithChannels.isEmpty()) {
+ for (Channel channel : channelDataManager.getChannelList()) {
+ if (newInputsWithChannels.contains(channel.getInputId())) {
+ channelDataManager.updateBrowsable(channel.getId(), true);
+ }
+ }
+ channelDataManager.applyUpdatedValuesToDb();
+ }
+ }
+
public boolean isFirstTune() {
return mIsFirstTune;
}
@@ -153,7 +197,9 @@ public class SetupUtils {
*/
public void markAsKnownInput(String inputId) {
mKnownInputs.add(inputId);
- mSharedPreferences.edit().putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs).apply();
+ mRecognizedInputs.add(inputId);
+ mSharedPreferences.edit().putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs)
+ .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs).apply();
}
/**
@@ -180,6 +226,37 @@ public class SetupUtils {
}
/**
+ * Checks whether the given input is already recognized by the user or not.
+ */
+ private boolean isRecognizedInput(String inputId) {
+ return mRecognizedInputs.contains(inputId);
+ }
+
+ /**
+ * Marks all the inputs as recognized inputs. Once it is marked, {@link #isRecognizedInput} will
+ * return {@code true}.
+ */
+ public void markAllInputsRecognized(TvInputManagerHelper inputManager) {
+ for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) {
+ mRecognizedInputs.add(input.getId());
+ }
+ mSharedPreferences.edit().putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs)
+ .apply();
+ }
+
+ /**
+ * Checks whether there are any unrecognized inputs.
+ */
+ public boolean hasUnrecognizedInput(TvInputManagerHelper inputManager) {
+ for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) {
+ if (!isRecognizedInput(input.getId())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* Grants permission for writing EPG data to all verified packages.
*
* @param context The Context used for granting permission.
@@ -251,8 +328,8 @@ public class SetupUtils {
* Called when input list is changed. It mainly handles input removals.
*/
public void onInputListUpdated(TvInputManager manager) {
- // mKnownInputs is a super set of mSetUpInputs.
- Set<String> removedInputList = new HashSet<>(mKnownInputs);
+ // mRecognizedInputs > mKnownInputs > mSetUpInputs.
+ Set<String> removedInputList = new HashSet<>(mRecognizedInputs);
for (TvInputInfo input : manager.getTvInputList()) {
removedInputList.remove(input.getId());
}
@@ -263,12 +340,13 @@ public class SetupUtils {
if (!removedInputList.isEmpty()) {
for (String input : removedInputList) {
+ mRecognizedInputs.remove(input);
mSetUpInputs.remove(input);
mKnownInputs.remove(input);
}
- mSharedPreferences.edit()
- .putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply();
- mSharedPreferences.edit().putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs).apply();
+ mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs)
+ .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs)
+ .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs).apply();
}
}
@@ -279,12 +357,20 @@ public class SetupUtils {
public void onSetupDone(String inputId) {
SoftPreconditions.checkState(inputId != null);
if (DEBUG) Log.d(TAG, "onSetupDone: input=" + inputId);
+ if (!mRecognizedInputs.contains(inputId)) {
+ Log.i(TAG, "An unrecognized input's setup has been done. inputId=" + inputId);
+ mRecognizedInputs.add(inputId);
+ mSharedPreferences.edit().putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs)
+ .apply();
+ }
if (!mKnownInputs.contains(inputId)) {
Log.i(TAG, "An unknown input's setup has been done. inputId=" + inputId);
mKnownInputs.add(inputId);
+ mSharedPreferences.edit().putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs).apply();
+ }
+ if (!mSetUpInputs.contains(inputId)) {
+ mSetUpInputs.add(inputId);
+ mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply();
}
- 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
index b00027a8..3643fca4 100644
--- a/src/com/android/tv/util/SoftPreconditions.java
+++ b/src/com/android/tv/util/SoftPreconditions.java
@@ -20,7 +20,7 @@ import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.BuildConfig;
+import com.android.tv.common.BuildConfig;
import com.android.tv.common.feature.Feature;
/**
@@ -147,8 +147,10 @@ public final class SoftPreconditions {
tag = TAG;
}
String logMessage;
- if (msg == null) {
+ if (TextUtils.isEmpty(msg)) {
logMessage = prefix;
+ } else if (TextUtils.isEmpty(prefix)) {
+ logMessage = msg;
} else {
logMessage = prefix + ": " + msg;
}
diff --git a/src/com/android/tv/util/SystemProperties.java b/src/com/android/tv/util/SystemProperties.java
index 235161b6..1dc70fd5 100644
--- a/src/com/android/tv/util/SystemProperties.java
+++ b/src/com/android/tv/util/SystemProperties.java
@@ -58,6 +58,13 @@ public final class SystemProperties {
public static final BooleanSystemProperty USE_TRACKER = new BooleanSystemProperty(
"tv_use_tracker", true);
+ /**
+ * Selects using {@link com.android.tv.dvr.DvrDataManagerInMemoryImpl}
+ * instead of {@link com.android.tv.dvr.DvrDataManagerImpl}
+ */
+ public static final BooleanSystemProperty USE_IN_MEMORY_DVR_DB = new BooleanSystemProperty(
+ "tv_use_in_memory_dvr_db", false); // STOPSHIP(DVR)
+
static {
updateSystemProperties();
}
diff --git a/src/com/android/tv/util/TvSettings.java b/src/com/android/tv/util/TvSettings.java
index 788a7d60..133fdd72 100644
--- a/src/com/android/tv/util/TvSettings.java
+++ b/src/com/android/tv/util/TvSettings.java
@@ -103,6 +103,7 @@ public final class TvSettings {
* {@link #PIP_LAYOUT_SIDE_BY_SIDE}. If the preference value does not exist,
* {@link #PIP_LAYOUT_BOTTOM_RIGHT} is returned.
*/
+ @SuppressWarnings("ResourceType")
@PipLayout
public static int getPipLayout(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
@@ -128,6 +129,7 @@ public final class TvSettings {
* {@link #PIP_SIZE_SMALL} and {@link #PIP_SIZE_BIG}. If the preference value does not
* exist, {@link #PIP_SIZE_SMALL} is returned.
*/
+ @SuppressWarnings("ResourceType")
@PipSize
public static int getPipSize(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
@@ -227,6 +229,7 @@ public final class TvSettings {
}
@ContentRatingLevel
+ @SuppressWarnings("ResourceType")
public static int getContentRatingLevel(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
PREF_CONTENT_RATING_LEVEL, CONTENT_RATING_LEVEL_NONE);
diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java
index 5c6d5345..44d601c5 100644
--- a/src/com/android/tv/util/Utils.java
+++ b/src/com/android/tv/util/Utils.java
@@ -50,8 +50,10 @@ import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.data.StreamInfo;
import com.android.usbtuner.tvinput.UsbTunerTvInputService;
+
import java.text.SimpleDateFormat;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
@@ -75,6 +77,7 @@ public class Utils {
public static final String EXTRA_KEY_ACTION = "action";
public static final String EXTRA_ACTION_SHOW_TV_INPUT ="show_tv_input";
public static final String EXTRA_KEY_FROM_LAUNCHER = "from_launcher";
+ public static final String EXTRA_KEY_RECORDING_URI = "recording_uri";
// Query parameter in the intent of starting MainActivity.
public static final String PARAM_SOURCE = "source";
@@ -450,36 +453,6 @@ public class Utils {
metadata.toString());
}
- public static TvContentRating[] stringToContentRatings(String commaSeparatedRatings) {
- if (TextUtils.isEmpty(commaSeparatedRatings)) {
- return null;
- }
- String[] ratings = commaSeparatedRatings.split("\\s*,\\s*");
- List<TvContentRating> contentRatings = new ArrayList<>();
- for (String rating : ratings) {
- try {
- contentRatings.add(TvContentRating.unflattenFromString(rating));
- } catch (IllegalArgumentException e) {
- Log.e(TAG, "Can't parse the content rating: '" + rating + "'", e);
- }
- }
- return contentRatings.size() == 0 ?
- null : contentRatings.toArray(new TvContentRating[contentRatings.size()]);
- }
-
- public static String contentRatingsToString(TvContentRating[] contentRatings) {
- if (contentRatings == null || contentRatings.length == 0) {
- return null;
- }
- final String DELIMITER = ",";
- StringBuilder ratings = new StringBuilder(contentRatings[0].flattenToString());
- for (int i = 1; i < contentRatings.length; ++i) {
- ratings.append(DELIMITER);
- ratings.append(contentRatings[i].flattenToString());
- }
- return ratings.toString();
- }
-
public static boolean isEqualLanguage(String lang1, String lang2) {
if (lang1 == null) {
return lang2 == null;