diff options
author | Nick Chalko <nchalko@google.com> | 2016-02-26 13:38:57 -0800 |
---|---|---|
committer | Nick Chalko <nchalko@google.com> | 2016-02-26 13:39:22 -0800 |
commit | ba5845f23b8fbc985890f892961abc8b39886611 (patch) | |
tree | da373b9fe1955a2c7008c2e65df5ec3f5b087454 /src/com/android/tv | |
parent | 1abddd9f6225298066094e20a6c29061b6af4590 (diff) | |
download | TV-ba5845f23b8fbc985890f892961abc8b39886611.tar.gz |
Sync to ub-tv-interns at cc7c29d2a24a1343498f6d95ca5a79e003e6aefe
Change-Id: I580da190231e47c65b69f425b30ec4685eb50ce4
Diffstat (limited to 'src/com/android/tv')
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; |