diff options
author | Nick Chalko <nchalko@google.com> | 2015-12-09 13:48:17 -0800 |
---|---|---|
committer | Nick Chalko <nchalko@google.com> | 2015-12-11 15:09:19 -0800 |
commit | 1abddd9f6225298066094e20a6c29061b6af4590 (patch) | |
tree | 97d701f8681cca9939c86e5e61523775d4c13aea /src | |
parent | 7d67089aa1e9aa2123c3cd2f386d7019a1544db1 (diff) | |
download | TV-1abddd9f6225298066094e20a6c29061b6af4590.tar.gz |
Sync to ub-tv-heroes at 1.08.301
source change id If9b64d7bbc6e8f77b360e502d34e5452775c0402
Change-Id: I4ffe87911cb85e54880d1d918d1b8fb7bb8cfb7d
Diffstat (limited to 'src')
67 files changed, 4262 insertions, 343 deletions
diff --git a/src/com/android/tv/ApplicationSingletons.java b/src/com/android/tv/ApplicationSingletons.java new file mode 100644 index 00000000..0ef61e72 --- /dev/null +++ b/src/com/android/tv/ApplicationSingletons.java @@ -0,0 +1,51 @@ +/* + * 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 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; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrSessionManager; +import com.android.tv.util.TvInputManagerHelper; + +/** + * Interface with getters for application scoped singletons. + */ +public interface ApplicationSingletons { + + Analytics getAnalytics(); + + ChannelDataManager getChannelDataManager(); + + DvrDataManager getDvrDataManager(); + + DvrManager getDvrManager(); + + DvrSessionManager getDvrSessionManger(); + + OptOutPreferenceHelper getOptPreferenceHelper(); + + ProgramDataManager getProgramDataManager(); + + Tracker getTracker(); + + TvInputManagerHelper getTvInputManagerHelper(); +} diff --git a/src/com/android/tv/ChannelTuner.java b/src/com/android/tv/ChannelTuner.java index d0d7c595..0195249b 100644 --- a/src/com/android/tv/ChannelTuner.java +++ b/src/com/android/tv/ChannelTuner.java @@ -17,14 +17,17 @@ package com.android.tv; 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.util.Log; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.util.CollectionUtils; import com.android.tv.util.SoftPreconditions; +import com.android.tv.util.TvInputManagerHelper; import java.util.ArrayList; import java.util.Collections; @@ -52,7 +55,11 @@ public class ChannelTuner { private final Handler mHandler = new Handler(); private final ChannelDataManager mChannelDataManager; private final Set<Listener> mListeners = CollectionUtils.createSmallSet(); + @Nullable private Channel mCurrentChannel; + private final TvInputManagerHelper mInputManager; + @Nullable + private TvInputInfo mCurrentChannelInputInfo; private final ChannelDataManager.Listener mChannelDataManagerListener = new ChannelDataManager.Listener() { @@ -79,8 +86,9 @@ public class ChannelTuner { } }; - public ChannelTuner(ChannelDataManager channelDataManager) { + public ChannelTuner(ChannelDataManager channelDataManager, TvInputManagerHelper inputManager) { mChannelDataManager = channelDataManager; + mInputManager = inputManager; } /** @@ -144,6 +152,7 @@ public class ChannelTuner { /** * Returns the current channel. */ + @Nullable public Channel getCurrentChannel() { return mCurrentChannel; } @@ -180,6 +189,14 @@ public class ChannelTuner { } /** + * Returns the current {@link TvInputInfo}. + */ + @Nullable + public TvInputInfo getCurrentInputInfo() { + return mCurrentChannelInputInfo; + } + + /** * Returns true, if the current channel is for a passthrough TV input. */ public boolean isCurrentChannelPassthrough() { @@ -336,6 +353,9 @@ public class ChannelTuner { } Channel previousChannel = mCurrentChannel; mCurrentChannel = channel; + if (mCurrentChannel != null) { + mCurrentChannelInputInfo = mInputManager.getTvInputInfo(mCurrentChannel.getInputId()); + } for (Listener l : mListeners) { l.onChannelChanged(previousChannel, mCurrentChannel); } diff --git a/src/com/android/tv/Features.java b/src/com/android/tv/Features.java index f62987ab..9564afaa 100644 --- a/src/com/android/tv/Features.java +++ b/src/com/android/tv/Features.java @@ -17,15 +17,19 @@ 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.PropertyFeature; -import com.android.tv.util.EngOnlyFeature; +import com.android.tv.common.feature.SharedPreferencesFeature; +import com.android.tv.common.feature.TestableFeature; /** * List of {@link Feature} for the Live TV App. @@ -38,7 +42,7 @@ public final class Features { * * <p>See <a href="http://b/20228119">b/20228119</a> */ - public static Feature ANALYTICS_OPT_OUT = new EngOnlyFeature(); + public static Feature ANALYTICS_OPT_OUT = ENG_ONLY_FEATURE; /** * Analytics that include sensitive information such as channel or program identifiers. @@ -47,8 +51,25 @@ public final class Features { */ 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 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 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; /** * A flag which indicates that the on-boarding experience is used or not. @@ -58,6 +79,9 @@ public final class Features { public static Feature ONBOARDING_EXPERIENCE = new PropertyFeature( "feature_tv_use_onboarding_exp", false); + public static Feature ONBOARDING_PLAY_STORE = AND(ONBOARDING_EXPERIENCE, OFF); + public static Feature ONBOARDING_USB_TUNER = AND(ONBOARDING_EXPERIENCE, USB_TUNER); + @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 92cbd462..6bb2995b 100644 --- a/src/com/android/tv/MainActivity.java +++ b/src/com/android/tv/MainActivity.java @@ -20,16 +20,20 @@ import android.app.Activity; import android.app.FragmentTransaction; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.PixelFormat; import android.graphics.Point; import android.hardware.display.DisplayManager; import android.media.AudioManager; +import android.media.MediaMetadata; import android.media.session.MediaSession; import android.media.session.PlaybackState; import android.media.tv.TvContentRating; @@ -47,6 +51,7 @@ import android.os.Message; import android.provider.Settings; import android.support.annotation.IntDef; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import android.view.Display; @@ -65,7 +70,9 @@ import android.widget.Toast; import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.SendConfigInfoRunnable; import com.android.tv.analytics.Tracker; +import com.android.tv.common.TvCommonUtils; import com.android.tv.common.WeakHandler; +import com.android.tv.common.dvr.DvrSessionClient; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.OnCurrentProgramUpdatedListener; @@ -75,6 +82,7 @@ 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.DvrManager; import com.android.tv.menu.Menu; import com.android.tv.onboarding.OnboardingActivity; import com.android.tv.parental.ContentRatingsManager; @@ -153,10 +161,17 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private static final float FRAME_RATE_FOR_FILM = 23.976f; private static final float FRAME_RATE_EPSILON = 0.1f; + private static final float MEDIA_SESSION_STOPPED_SPEED = 0.0f; + private static final float MEDIA_SESSION_PLAYING_SPEED = 1.0f; + 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 USB_TV_TUNER_INPUT_ID = + "com.android.tv/com.android.usbtuner.tvinput.UsbTunerTvInputService"; + private static final String DVR_TEST_INPUT_ID = USB_TV_TUNER_INPUT_ID; + // Tracker screen names. public static final String SCREEN_NAME = "Main"; private static final String SCREEN_BEHIND_NAME = "Behind"; @@ -222,9 +237,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC 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 TunableTvView mTvView; private TunableTvView mPipView; @@ -234,12 +251,17 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC // TODO: Move the scene views into TvTransitionManager or TvOverlayManager. private ChannelBannerView mChannelBannerView; private KeypadChannelSwitchView mKeypadChannelSwitchView; + @Nullable private Uri mInitChannelUri; + @Nullable + private String mParentInputIdWhenScreenOff; private boolean mShowProgramGuide; private boolean mShowSelectInputView; private TvInputInfo mInputToSetUp; private final List<MemoryManageable> mMemoryManageables = new ArrayList<>(); private MediaSession mMediaSession; + private int mNowPlayingCardWidth; + private int mNowPlayingCardHeight; private String mInputIdUnderSetup; private boolean mIsSetupActivityCalledByDialog; @@ -259,6 +281,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private boolean mBackKeyPressed; private boolean mNeedShowBackKeyGuide; private boolean mVisibleBehind; + private boolean mAc3PassthroughSupported; private boolean mIsFilmModeSet; private float mDefaultRefreshRate; @@ -302,6 +325,14 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC // MediaPlayer, a device may not go to the sleep mode and audio can be heard, // because MediaPlayer keeps playing media by its wake lock. mInitChannelUri = mChannelTuner.getCurrentChannelUri(); + if (mChannelTuner.isCurrentChannelPassthrough()) { + // When ACTION_SCREEN_OFF is invoked, some CEC devices may be already + // removed. So we need to get the input info from ChannelTuner instead of + // TvInputManagerHelper. + TvInputInfo input = mChannelTuner.getCurrentInputInfo(); + mParentInputIdWhenScreenOff = input.getParentId(); + if (DEBUG) Log.d(TAG, "Parent input: " + mParentInputIdWhenScreenOff); + } stopAll(true); } else if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) { if (DEBUG) Log.d(TAG, "Received ACTION_SCREEN_ON"); @@ -396,7 +427,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC tvApplication.setMainActivity(this); if (Features.ONBOARDING_EXPERIENCE.isEnabled(this) - && OnboardingUtils.needToShowOnboarding(this)) { + && OnboardingUtils.needToShowOnboarding(this) + && !TvCommonUtils.isRunningInTest()) { + // TODO: We turn off the new onboarding for test, because tests are broken by + // the new onboarding. We need to enable the feature for tests later. startActivity(OnboardingActivity.buildIntent(this, getIntent())); finish(); return; @@ -412,13 +446,14 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mProgramDataManager.addOnCurrentProgramUpdatedListener(Channel.INVALID_ID, mOnCurrentProgramUpdatedListener); mProgramDataManager.setPrefetchEnabled(true); - mChannelTuner = new ChannelTuner(mChannelDataManager); + mChannelTuner = new ChannelTuner(mChannelDataManager, mTvInputManagerHelper); mChannelTuner.addListener(mChannelTunerListener); mChannelTuner.start(); mPipInputManager = new PipInputManager(this, mTvInputManagerHelper, mChannelTuner); mPipInputManager.start(); mMemoryManageables.add(mProgramDataManager); mMemoryManageables.add(ImageCache.getInstance()); + mDvrManager = tvApplication.getDvrManager(); DisplayManager displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE); Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); @@ -540,13 +575,33 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; + mMediaSession = new MediaSession(this, MEDIA_SESSION_TAG); + mMediaSession.setCallback(new MediaSession.Callback() { + @Override + public boolean onMediaButtonEvent(Intent mediaButtonIntent) { + // Consume the media button event here. Should not send it to other apps. + return true; + } + }); + mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | + MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); + mNowPlayingCardWidth = getResources().getDimensionPixelSize( + R.dimen.notif_card_img_max_width); + mNowPlayingCardHeight = getResources().getDimensionPixelSize(R.dimen.notif_card_img_height); + mTvViewUiManager.restoreDisplayMode(false); if (!handleIntent(getIntent())) { finish(); return; } - mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, null); + mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, + new AudioCapabilitiesReceiver.OnAc3PassthroughCapabilityChangeListener() { + @Override + public void onAc3PassthroughCapabilityChange(boolean capability) { + mAc3PassthroughSupported = capability; + } + }); mAudioCapabilitiesReceiver.register(); mAccessibilityManager = @@ -714,30 +769,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } }); } - if (mMediaSession == null) { - mMediaSession = new MediaSession(this, MEDIA_SESSION_TAG); - PlaybackState playbackState = new PlaybackState.Builder() - .setActions(PlaybackState.ACTION_PAUSE - | PlaybackState.ACTION_PLAY - | PlaybackState.ACTION_PLAY_PAUSE - | PlaybackState.ACTION_FAST_FORWARD - | PlaybackState.ACTION_REWIND - | PlaybackState.ACTION_SKIP_TO_NEXT - | PlaybackState.ACTION_SKIP_TO_PREVIOUS - | PlaybackState.ACTION_STOP) - .setState(PlaybackState.STATE_PLAYING, 0, 1) - .build(); - mMediaSession.setPlaybackState(playbackState); - mMediaSession.setCallback(new MediaSession.Callback() { - @Override - public boolean onMediaButtonEvent(Intent mediaButtonIntent) { - // Consume media button events to avoid to send the events to another app. - return true; - } - }); - mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS); - mMediaSession.setActive(true); - } } @Override @@ -756,6 +787,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (!mVisibleBehind) { mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; mAudioManager.abandonAudioFocus(this); + if (mMediaSession.isActive()) { + mMediaSession.setActive(false); + } mTracker.sendScreenView(""); } else { mTracker.sendScreenView(SCREEN_BEHIND_NAME); @@ -788,6 +822,22 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (DEBUG) Log.d(TAG, "resumeTvIfNeeded()"); if (!mTvView.isPlaying() || mInitChannelUri != null || (mLaunchedByLauncher && mChannelTuner.isCurrentChannelPassthrough())) { + if (TvContract.isChannelUriForPassthroughInput(mInitChannelUri)) { + // The target input may not be ready yet, especially, just after screen on. + String inputId = mInitChannelUri.getPathSegments().get(1); + TvInputInfo input = mTvInputManagerHelper.getTvInputInfo(inputId); + if (input == null) { + input = mTvInputManagerHelper.getTvInputInfo(mParentInputIdWhenScreenOff); + if (input == null) { + SoftPreconditions.checkState(false, TAG, "Input disappear." + input); + finish(); + } else { + mInitChannelUri = + TvContract.buildChannelUriForPassthroughInput(input.getId()); + } + } + } + mParentInputIdWhenScreenOff = null; startTv(mInitChannelUri); mInitChannelUri = null; } @@ -901,10 +951,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC @Override protected void onStop() { if (DEBUG) Log.d(TAG, "onStop()"); - if (mMediaSession != null) { - mMediaSession.release(); - mMediaSession = null; - } mActivityStarted = false; stopAll(false); unregisterReceiver(mBroadcastReceiver); @@ -928,11 +974,14 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC * @param calledByDialog If true, startSetupActivity is invoked from the setup dialog. */ public void startSetupActivity(TvInputInfo input, boolean calledByDialog) { - Intent intent = input.createSetupIntent(); + Intent intent = TvCommonUtils.createSetupIntent(input); if (intent == null) { Toast.makeText(this, R.string.msg_no_setup_activity, Toast.LENGTH_SHORT).show(); return; } + // Even though other app can handle the intent, the setup launched by Live channels + // should go through Live channels SetupPassthroughActivity. + intent.setComponent(new ComponentName(this, SetupPassthroughActivity.class)); try { // Now we know that the user intends to set up this input. Grant permission for writing // EPG data. @@ -945,7 +994,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC // immediately if other activity is about to start. And this activity is scheduled to // to be stopped again after onPause(). stopTv("startSetupActivity()", false); - startActivityForResultSafe(intent, REQUEST_CODE_START_SETUP_ACTIVITY); + startActivityForResult(intent, REQUEST_CODE_START_SETUP_ACTIVITY); } catch (ActivityNotFoundException e) { mInputIdUnderSetup = null; Toast.makeText(this, getString(R.string.msg_unable_to_start_setup_activity, @@ -1017,6 +1066,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } /** + * Returns true if the current connected TV supports AC3 passthough. + */ + public boolean isAc3PassthroughSupported() { + return mAc3PassthroughSupported; + } + + /** * Returns the current program which the user is watching right now.<p> * * If the time shifting is available, it can be a past program. @@ -1184,28 +1240,22 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC switch (requestCode) { case REQUEST_CODE_START_SETUP_ACTIVITY: if (resultCode == RESULT_OK) { - final String inputId = mInputIdUnderSetup; - SetupUtils.getInstance(this).onTvInputSetupFinished(inputId, new Runnable() { - @Override - public void run() { - int count = mChannelDataManager.getChannelCountForInput(inputId); - String text; - if (count > 0) { - text = getResources().getQuantityString(R.plurals.msg_channel_added, - count, count); - } else { - text = getString(R.string.msg_no_channel_added); - } - Toast.makeText(MainActivity.this, text, Toast.LENGTH_SHORT).show(); - mInputIdUnderSetup = null; - if (mChannelTuner.getCurrentChannel() == null) { - mChannelTuner.moveToAdjacentBrowsableChannel(true); - } - if (mTunePending) { - tune(); - } - } - }); + int count = mChannelDataManager.getChannelCountForInput(mInputIdUnderSetup); + String text; + if (count > 0) { + text = getResources().getQuantityString(R.plurals.msg_channel_added, + count, count); + } else { + text = getString(R.string.msg_no_channel_added); + } + Toast.makeText(MainActivity.this, text, Toast.LENGTH_SHORT).show(); + mInputIdUnderSetup = null; + if (mChannelTuner.getCurrentChannel() == null) { + mChannelTuner.moveToAdjacentBrowsableChannel(true); + } + if (mTunePending) { + tune(); + } } else { mInputIdUnderSetup = null; } @@ -1448,6 +1498,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC requestVisibleBehind(false); } mAudioManager.abandonAudioFocus(this); + if (mMediaSession.isActive()) { + mMediaSession.setActive(false); + } } mChannelTuner.resetCurrentChannel(); mTunePending = false; @@ -1510,12 +1563,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC @Override public void onContentBlocked() { - // Do nothing. + updateMediaSession(); } @Override public void onContentAllowed() { - // Do nothing. + updateMediaSession(); } }); if (!success) { @@ -1549,7 +1602,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC /** * Says {@code text} when accessibility is turned on. */ - public void sendAccessiblityText(String text) { + public void sendAccessibilityText(String text) { if (mAccessibilityManager.isEnabled()) { AccessibilityEvent event = AccessibilityEvent.obtain(); event.setClassName(getClass().getName()); @@ -1687,7 +1740,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mTracker.sendChannelTuneTime(info.getCurrentChannel(), mTuneDurationTimer.reset()); } - // If updatChannelBanner() is called without delay, the stream info seems flickering + // If updateChannelBanner() is called without delay, the stream info seems flickering // when the channel is quickly changed. if (!mHandler.hasMessages(MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE) && info.isVideoAvailable()) { @@ -1784,6 +1837,93 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC // be called during the pause state by mBroadcastReceiver (Intent.ACTION_SCREEN_ON). requestVisibleBehind(true); } + updateMediaSession(); + } + + private void updateMediaSession() { + if (getCurrentChannel() == null) { + mMediaSession.setActive(false); + return; + } + + // If the channel is blocked, display a lock and a short text on the Now Playing Card + if (mTvView.isScreenBlocked() || mTvView.getBlockedContentRating() != null) { + setMediaSessionPlaybackState(false); + + Bitmap art = BitmapFactory.decodeResource( + getResources(), R.drawable.ic_message_lock_preview); + updateMediaMetadata( + getResources().getString(R.string.channel_banner_locked_channel_title), art); + mMediaSession.setActive(true); + return; + } + + Program program = getCurrentProgram(); + String cardTitleText = program == null ? null : program.getTitle(); + if (TextUtils.isEmpty(cardTitleText)) { + cardTitleText = getCurrentChannel().getDisplayName(); + } + updateMediaMetadata(cardTitleText, null); + setMediaSessionPlaybackState(true); + + 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); + } + } + }); + } else { + updateMediaMetadataWithAlternativeArt(program); + } + + mMediaSession.setActive(true); + } + + private void updateMediaMetadata(String title, Bitmap posterArt) { + MediaMetadata.Builder builder = new MediaMetadata.Builder(); + builder.putString(MediaMetadata.METADATA_KEY_TITLE, title); + if (posterArt != null) { + builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt); + } + mMediaSession.setMetadata(builder.build()); + } + + private void updateMediaMetadataWithAlternativeArt(final Program program) { + Channel channel = getCurrentChannel(); + if (channel == null || program != getCurrentProgram()) { + return; + } + + String cardTitleText = program == null ? null : program.getTitle(); + if (TextUtils.isEmpty(cardTitleText)) { + cardTitleText = channel.getDisplayName(); + } + + Bitmap posterArt = BitmapFactory.decodeResource( + getResources(), R.drawable.default_now_card); + updateMediaMetadata(cardTitleText, posterArt); + } + + private void setMediaSessionPlaybackState(boolean isPlaying) { + PlaybackState.Builder builder = new PlaybackState.Builder(); + builder.setState(isPlaying ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_STOPPED, + PlaybackState.PLAYBACK_POSITION_UNKNOWN, + isPlaying ? MEDIA_SESSION_PLAYING_SPEED : MEDIA_SESSION_STOPPED_SPEED); + mMediaSession.setPlaybackState(builder.build()); } private void addToRecentChannels(long channelId) { @@ -1824,6 +1964,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC tvView.blockScreen(); if (tvView == mTvView) { updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); + updateMediaSession(); } } @@ -1831,6 +1972,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC tvView.unblockScreen(); if (tvView == mTvView) { updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); + updateMediaSession(); } } @@ -2076,6 +2218,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mKeypadChannelSwitchView.setChannels(null); } mMemoryManageables.clear(); + if (mMediaSession != null) { + mMediaSession.release(); + } if (mAudioCapabilitiesReceiver != null) { mAudioCapabilitiesReceiver.unregister(); } @@ -2150,8 +2295,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 */ if (SystemProperties.LOG_KEYEVENT.getValue()) { Log.d(TAG, "onKeyUp(" + keyCode + ", " + event + ")"); @@ -2288,6 +2436,14 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mOverlayManager.getSideFragmentManager().show(new MultiAudioFragment()); return true; } + case KeyEvent.KEYCODE_GUIDE: { + mOverlayManager.showProgramGuide(); + return true; + } + case KeyEvent.KEYCODE_INFO: { + mOverlayManager.showBanner(); + return true; + } } } if (SystemProperties.USE_DEBUG_KEYS.getValue()) { @@ -2328,6 +2484,55 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC case KeyEvent.KEYCODE_D: mOverlayManager.getSideFragmentManager().show(new DebugOptionFragment()); return true; + + 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; + } + } + 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) { + 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; + } } } return super.onKeyUp(keyCode, event); @@ -2631,12 +2836,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC public void onVisibleBehindCanceled() { stopTv("onVisibleBehindCanceled()", false); mTracker.sendScreenView(""); - if (mMediaSession != null) { - mMediaSession.release(); - mMediaSession = null; - } mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; mAudioManager.abandonAudioFocus(this); + if (mMediaSession.isActive()) { + mMediaSession.setActive(false); + } stopPip(); mVisibleBehind = false; super.onVisibleBehindCanceled(); @@ -2756,7 +2960,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC // Initialize TV app for test. The setup process should be finished before the Live TV app is // started. We only enable all the channels here. private void initForTest() { - if (!Utils.isRunningInTest()) { + if (!TvCommonUtils.isRunningInTest()) { return; } diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java index 45b99776..0c7a5d65 100644 --- a/src/com/android/tv/SetupPassthroughActivity.java +++ b/src/com/android/tv/SetupPassthroughActivity.java @@ -25,6 +25,7 @@ import android.util.Log; import com.android.tv.common.TvCommonConstants; import com.android.tv.util.SetupUtils; +import com.android.tv.util.SoftPreconditions; import com.android.tv.util.TvInputManagerHelper; /** @@ -33,7 +34,7 @@ import com.android.tv.util.TvInputManagerHelper; * <p> After setup activity is finished, all channels will be browsable. */ public class SetupPassthroughActivity extends Activity { - private static final String TAG = "SetupPassthroughActivity"; + private static final String TAG = "SetupPassthroughAct"; private static final boolean DEBUG = false; private static final int REQUEST_START_SETUP_ACTIVITY = 200; @@ -44,11 +45,12 @@ public class SetupPassthroughActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - TvApplication tvApplication = (TvApplication) getApplication(); - TvInputManagerHelper inputManager = tvApplication.getTvInputManagerHelper(); - // It is not only for setting the variable but also for early initialization of - // ChannelDataManager. - String inputId = getIntent().getStringExtra(TvCommonConstants.EXTRA_INPUT_ID); + Intent intent = getIntent(); + SoftPreconditions.checkState( + intent.getAction().equals(TvCommonConstants.INTENT_ACTION_INPUT_SETUP)); + ApplicationSingletons appSingletons = TvApplication.getSingletons(this); + TvInputManagerHelper inputManager = appSingletons.getTvInputManagerHelper(); + String inputId = intent.getStringExtra(TvCommonConstants.EXTRA_INPUT_ID); mTvInputInfo = inputManager.getTvInputInfo(inputId); if (DEBUG) Log.d(TAG, "TvInputId " + inputId + " / TvInputInfo " + mTvInputInfo); if (mTvInputInfo == null) { @@ -56,23 +58,29 @@ public class SetupPassthroughActivity extends Activity { finish(); return; } - Intent setupIntent = mTvInputInfo.createSetupIntent(); + Intent setupIntent = intent.getExtras().getParcelable(TvCommonConstants.EXTRA_SETUP_INTENT); + if (DEBUG) Log.d(TAG, "Setup activity launch intent: " + setupIntent); if (setupIntent == null) { Log.w(TAG, "The input (" + mTvInputInfo.getId() + ") doesn't have setup."); finish(); return; } SetupUtils.grantEpgPermission(this, mTvInputInfo.getServiceInfo().packageName); - mActivityAfterCompletion = getIntent().getParcelableExtra( + mActivityAfterCompletion = intent.getParcelableExtra( TvCommonConstants.EXTRA_ACTIVITY_AFTER_COMPLETION); if (DEBUG) Log.d(TAG, "Activity after completion " + mActivityAfterCompletion); - setupIntent.putExtras(getIntent().getExtras()); + // If EXTRA_SETUP_INTENT is not removed, an infinite recursion happens during + // setupIntent.putExtras(intent.getExtras()). + Bundle extras = intent.getExtras(); + extras.remove(TvCommonConstants.EXTRA_SETUP_INTENT); + setupIntent.putExtras(extras); startActivityForResult(setupIntent, REQUEST_START_SETUP_ACTIVITY); } @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { + public void onActivityResult(int requestCode, final int resultCode, final Intent data) { if (requestCode != REQUEST_START_SETUP_ACTIVITY || resultCode != Activity.RESULT_OK) { + setResult(resultCode, data); finish(); return; } @@ -86,6 +94,7 @@ public class SetupPassthroughActivity extends Activity { Log.w(TAG, "Activity launch failed", e); } } + setResult(resultCode, data); finish(); } }); diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java index 9c699389..e710026f 100644 --- a/src/com/android/tv/TvApplication.java +++ b/src/com/android/tv/TvApplication.java @@ -18,15 +18,18 @@ package com.android.tv; import android.app.Activity; import android.app.Application; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; 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.Nullable; import android.util.Log; import android.view.KeyEvent; @@ -35,19 +38,33 @@ 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.TvCommonUtils; 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.DvrManager; +import com.android.tv.dvr.DvrRecordingService; +import com.android.tv.dvr.DvrSessionManager; +import com.android.tv.util.SetupUtils; import com.android.tv.util.SystemProperties; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; import java.util.List; -public class TvApplication extends Application { +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. + */ + public static ApplicationSingletons getSingletons(Context context) { + return (ApplicationSingletons) context.getApplicationContext(); + } + private MainActivity mMainActivity; private SelectInputActivity mSelectInputActivity; private Analytics mAnalytics; @@ -56,10 +73,22 @@ public class TvApplication extends Application { private ChannelDataManager mChannelDataManager; private ProgramDataManager mProgramDataManager; private OptOutPreferenceHelper mOptPreferenceHelper; + private DvrManager mDvrManager; + private DvrDataManagerImpl mDvrDataManager; + @Nullable + private DvrSessionManager mDvrSessionManager; @Override public void onCreate() { super.onCreate(); + try { + PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); + versionName = pInfo.versionName; + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Unable to get version name.", e); + versionName = ""; + } + Log.i(TAG, "Starting Live TV " + getVersionName()); // Only set StrictMode for ENG builds because the build server only produces userdebug // builds. if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) { @@ -68,7 +97,7 @@ public class TvApplication extends Application { StrictMode.VmPolicy.Builder vmPolicyBuilder = new StrictMode.VmPolicy.Builder() .detectAll().penaltyLog(); if (BuildConfig.ENG && SystemProperties.ALLOW_DEATH_PENALTY.getValue() && - !Utils.isRunningInTest()) { + !TvCommonUtils.isRunningInTest()) { // TODO turn on death penalty for tests when they stop leaking MainActivity } StrictMode.setVmPolicy(vmPolicyBuilder.build()); @@ -100,21 +129,47 @@ public class TvApplication extends Application { } }.execute(); } - try { - PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); - versionName = pInfo.versionName; - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Unable to get version name.", e); - versionName = ""; - } mTvInputManagerHelper = new TvInputManagerHelper(this); mTvInputManagerHelper.start(); + mTvInputManagerHelper.addCallback(new TvInputCallback() { + @Override + public void onInputAdded(String inputId) { + handleInputCountChanged(); + } + + @Override + public void onInputRemoved(String inputId) { + handleInputCountChanged(); + } + }); if (DEBUG) Log.i(TAG, "Started Live TV " + versionName); + if (Features.DVR.isEnabled(this)) { + mDvrManager = new DvrManager(this); + //NOTE: DvrRecordingService just keeps running. + DvrRecordingService.startService(this); + } + } + + /** + * Returns the {@link DvrManager}. + */ + @Override + public DvrManager getDvrManager() { + return mDvrManager; + } + + @Override + public DvrSessionManager getDvrSessionManger() { + if (mDvrSessionManager == null) { + mDvrSessionManager = new DvrSessionManager(this); + } + return mDvrSessionManager; } /** * Returns the {@link Analytics}. */ + @Override public Analytics getAnalytics() { return mAnalytics; } @@ -122,10 +177,12 @@ public class TvApplication extends Application { /** * Returns the default tracker. */ + @Override public Tracker getTracker() { return mTracker; } + @Override public OptOutPreferenceHelper getOptPreferenceHelper(){ return mOptPreferenceHelper; } @@ -133,6 +190,7 @@ public class TvApplication extends Application { /** * Returns {@link ChannelDataManager}. */ + @Override public ChannelDataManager getChannelDataManager() { if (mChannelDataManager == null) { mChannelDataManager = new ChannelDataManager(this, mTvInputManagerHelper, mTracker); @@ -144,6 +202,7 @@ public class TvApplication extends Application { /** * Returns {@link ProgramDataManager}. */ + @Override public ProgramDataManager getProgramDataManager() { if (mProgramDataManager == null) { mProgramDataManager = new ProgramDataManager(this); @@ -153,8 +212,21 @@ public class TvApplication extends Application { } /** + * Returns {@link DvrDataManager}. + */ + @Override + public DvrDataManager getDvrDataManager() { + if (mDvrDataManager == null) { + mDvrDataManager = new DvrDataManagerImpl(this); + mDvrDataManager.start(); + } + return mDvrDataManager; + } + + /** * Returns {@link TvInputManagerHelper}. */ + @Override public TvInputManagerHelper getTvInputManagerHelper() { return mTvInputManagerHelper; } @@ -255,4 +327,30 @@ public class TvApplication extends Application { return versionName; } + /** + * Checks the input counts and enable/disable TvActivity. Also updates the input list in + * {@link SetupUtils}. + */ + public void handleInputCountChanged() { + TvInputManager inputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE); + if (!Features.UNHIDE.isEnabled(TvApplication.this)) { + 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); + } + } + SetupUtils.getInstance(TvApplication.this).onInputListUpdated(inputManager); + } } diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java index 7901a01f..7411d297 100644 --- a/src/com/android/tv/data/Channel.java +++ b/src/com/android/tv/data/Channel.java @@ -32,6 +32,7 @@ import android.text.TextUtils; import android.util.Log; import com.android.tv.common.TvCommonConstants; +import com.android.tv.dvr.provider.DvrContract; import com.android.tv.util.ImageLoader; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -113,6 +114,15 @@ public final class Channel { } /** + * Use this projection if you want to create {@link Channel} object using + * {@link #fromDvrCursor}. + */ + public static final String[] PROJECTION_DVR = { + // Columns must match what is read in Channel.fromDvrCursor() + DvrContract.DvrChannels._ID + }; + + /** * Creates {@code Channel} object from cursor. * * <p>The query that created the cursor MUST use {@link #PROJECTION} @@ -142,6 +152,16 @@ public final class Channel { return channel; } + /** + * Creates a {@link Channel} object from the DVR database. + */ + public static Channel fromDvrCursor(Cursor c) { + Channel channel = new Channel(); + int index = -1; + channel.mDvrId = c.getLong(++index); + return channel; + } + /** ID of this channel. Matches to BaseColumns._ID. */ private long mId; @@ -163,6 +183,13 @@ public final class Channel { private Intent mAppLinkIntent; private int mAppLinkType; + private long mDvrId; + + /** + * TODO(DVR): Need to fill the following data. + */ + private boolean mRecordable; + public interface LoadImageCallback { void onLoadImageFinished(Channel channel, int type, Bitmap logo); } @@ -238,6 +265,13 @@ public final class Channel { } /** + * Returns an ID in DVR database. + */ + public long getDvrId() { + return mDvrId; + } + + /** * Checks if two channels equal by checking ids. */ @Override @@ -566,6 +600,7 @@ public final class Channel { return; } } catch (URISyntaxException e) { + Log.w(TAG, "Unable to set app link for " + mAppLinkIntentUri, e); // Do nothing. } } diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/Program.java index 82638e14..a0a5c090 100644 --- a/src/com/android/tv/data/Program.java +++ b/src/com/android/tv/data/Program.java @@ -27,6 +27,7 @@ import android.text.TextUtils; import android.util.Log; import com.android.tv.R; +import com.android.tv.dvr.provider.DvrContract; import com.android.tv.util.ImageLoader; import com.android.tv.util.Utils; @@ -60,6 +61,15 @@ public final class Program implements Comparable<Program> { }; /** + * Use this projection if you want to create {@link Program} object using + * {@link #fromDvrCursor}. + */ + public static final String[] PROJECTION_DVR = { + // Columns must match what is read in Channel.fromDvrCursor() + DvrContract.DvrPrograms._ID + }; + + /** * Creates {@code Program} object from cursor. * * <p>The query that created the cursor MUST use {@link #PROJECTION}. @@ -85,6 +95,16 @@ public final class Program implements Comparable<Program> { return builder.build(); } + /** + * Creates a {@link Program} object from the DVR database. + */ + public static Program fromDvrCursor(Cursor c) { + Program program = new Program(); + int index = -1; + program.mDvrId = c.getLong(++index); + return program; + } + private long mChannelId; private String mTitle; private String mEpisodeTitle; @@ -100,6 +120,14 @@ public final class Program implements Comparable<Program> { private int[] mCanonicalGenreIds; private TvContentRating[] mContentRatings; + private long mDvrId; + + /** + * TODO(DVR): Need to fill the following data. + */ + private boolean mRecordable; + private boolean mRecordingScheduled; + public interface LoadPosterArtCallback { void onLoadPosterArtFinished(Program program, Bitmap posterArt); } @@ -213,6 +241,13 @@ public final class Program implements Comparable<Program> { return false; } + /** + * Returns an ID in DVR database. + */ + public long getDvrId() { + return mDvrId; + } + @Override public int hashCode() { return Objects.hash(mChannelId, mStartTimeUtcMillis, mEndTimeUtcMillis, diff --git a/src/com/android/tv/data/TvInputNewComparator.java b/src/com/android/tv/data/TvInputNewComparator.java new file mode 100644 index 00000000..11993d00 --- /dev/null +++ b/src/com/android/tv/data/TvInputNewComparator.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.data; + +import android.media.tv.TvInputInfo; + +import com.android.tv.util.SetupUtils; +import com.android.tv.util.TvInputManagerHelper; + +import java.util.Comparator; + +/** + * Compares TV input such that the new input comes first. + */ +public class TvInputNewComparator implements Comparator<TvInputInfo> { + private final SetupUtils mSetupUtils; + private final TvInputManagerHelper mInputManager; + + public TvInputNewComparator(SetupUtils setupUtils, TvInputManagerHelper inputManager) { + mSetupUtils = setupUtils; + mInputManager = inputManager; + } + + @Override + public int compare(TvInputInfo lhs, TvInputInfo rhs) { + boolean lhsIsNewInput = mSetupUtils.isNewInput(lhs.getId()); + boolean rhsIsNewInput = mSetupUtils.isNewInput(rhs.getId()); + if (lhsIsNewInput != rhsIsNewInput) { + return lhsIsNewInput ? -1 : 1; + } + return mInputManager.getDefaultTvInputInfoComparator().compare(lhs, rhs); + } +} diff --git a/src/com/android/tv/dialog/SafeDismissDialogFragment.java b/src/com/android/tv/dialog/SafeDismissDialogFragment.java index bd1c55a6..1569f0a9 100644 --- a/src/com/android/tv/dialog/SafeDismissDialogFragment.java +++ b/src/com/android/tv/dialog/SafeDismissDialogFragment.java @@ -48,7 +48,7 @@ public abstract class SafeDismissDialogFragment extends DialogFragment super.onAttach(activity); mAttached = true; mActivity = (MainActivity) activity; - mTracker = ((TvApplication) activity.getApplication()).getTracker(); + mTracker = TvApplication.getSingletons(activity).getTracker(); if (mDismissPending) { mDismissPending = false; dismiss(); diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java new file mode 100644 index 00000000..0ce5d787 --- /dev/null +++ b/src/com/android/tv/dvr/BaseDvrDataManager.java @@ -0,0 +1,81 @@ +/* + * 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.content.Context; +import android.util.Log; + +import com.android.tv.Features; +import com.android.tv.util.CollectionUtils; +import com.android.tv.util.SoftPreconditions; + +import java.util.Set; + +/** + * Base implementation of @{link DataManagerInternal}. + */ +public abstract class BaseDvrDataManager implements WritableDvrDataManager { + private final static String TAG = "BaseDvrDataManager"; + private final static boolean DEBUG = false; + + private final Set<DvrDataManager.Listener> mListeners = CollectionUtils.createSmallSet(); + + BaseDvrDataManager (Context context){ + SoftPreconditions.checkFeatureEnabled(context,Features.DVR, TAG); + } + + @Override + public final void addListener(DvrDataManager.Listener listener) { + mListeners.add(listener); + } + + @Override + public final void removeListener(DvrDataManager.Listener listener) { + mListeners.remove(listener); + } + + /** + * Calls {@link DvrDataManager.Listener#onRecordingAdded(Recording)} for each current listener. + */ + protected final void notifyRecordingAdded(Recording recording) { + for (Listener l : mListeners) { + if (DEBUG) Log.d(TAG, "notify " + l + "added recording " + recording); + l.onRecordingAdded(recording); + } + } + + /** + * Calls {@link DvrDataManager.Listener#onRecordingRemoved(Recording)} for each current listener. + */ + protected final void notifyRecordingRemoved(Recording recording) { + for (Listener l : mListeners) { + if (DEBUG) Log.d(TAG, "notify " + l + "removed recording " + recording); + l.onRecordingRemoved(recording); + } + } + + /** + * Calls {@link DvrDataManager.Listener#onRecordingStatusChanged(Recording)} for each current + * listener. + */ + protected final void notifyRecordingStatusChanged(Recording recording) { + for (Listener l : mListeners) { + if (DEBUG) Log.d(TAG, "notify " + l + "changed recording " + recording); + l.onRecordingStatusChanged(recording); + } + } +} diff --git a/src/com/android/tv/dvr/DvrDataManager.java b/src/com/android/tv/dvr/DvrDataManager.java new file mode 100644 index 00000000..bed3ed80 --- /dev/null +++ b/src/com/android/tv/dvr/DvrDataManager.java @@ -0,0 +1,90 @@ +/* + * 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.util.Range; + +import java.util.List; + +/** + * Read only data manager. + */ +public interface DvrDataManager { + long NEXT_START_TIME_NOT_FOUND = -1; + + boolean isInitialized(); + + /** + * Returns recordings. + */ + List<Recording> getRecordings(); + + /** + * Returns past recordings. + */ + List<Recording> getFinishedRecordings(); + + /** + * Returns started recordings. + */ + List<Recording> getStartedRecordings(); + + /** + * Returns scheduled recordings + */ + List<Recording> getScheduledRecordings(); + + /** + * Returns season recordings. + */ + List<SeasonRecording> getSeasonRecordings(); + + /** + * Returns the next start time after {@code time} or {@link #NEXT_START_TIME_NOT_FOUND} + * if none is found. + * + * @param time time milliseconds + */ + long getNextScheduledStartTimeAfter(long time); + + /** + * Returns a list of all Recordings with a overlap with the given time period inclusive. + * + * <p> A recording overlaps with a period when + * {@code recording.getStartTime() <= period.getUpper() && + * recording.getEndTime() >= period.getLower()}. + * + * @param period a time period in milliseconds. + */ + List<Recording> getRecordingsThatOverlapWith(Range<Long> period); + + /** + * Add a {@link Listener}. + */ + void addListener(Listener listener); + + /** + * Remove a {@link Listener}. + */ + void removeListener(Listener listener); + + interface Listener { + void onRecordingAdded(Recording recording); + void onRecordingRemoved(Recording recording); + void onRecordingStatusChanged(Recording recording); + } +} diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java new file mode 100644 index 00000000..d1c590af --- /dev/null +++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java @@ -0,0 +1,154 @@ +/* + * 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.content.Context; +import android.support.annotation.VisibleForTesting; +import android.util.Range; + +import com.android.tv.dvr.Recording.RecordingState; +import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryTask; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * DVR Data manager to handle recordings and schedules. + */ +public class DvrDataManagerImpl extends BaseDvrDataManager { + private Context mContext; + private boolean mLoadFinished; + private final List<Recording> mRecordings = new ArrayList<>(); + private AsyncDvrQueryTask mQueryTask; + + public DvrDataManagerImpl(Context context) { + super(context); + mContext = context; + } + + public void start() { + mQueryTask = new AsyncDvrQueryTask(mContext) { + @Override + protected void onPostExecute(List<Recording> result) { + mQueryTask = null; + mLoadFinished = true; + mRecordings.addAll(result); + Collections.sort(mRecordings, Recording.START_TIME_COMPARATOR); + } + }; + mQueryTask.executeOnDbThread(); + } + + public void stop() { + if (mQueryTask != null) { + mQueryTask.cancel(true); + mQueryTask = null; + } + } + + @Override + public boolean isInitialized() { + return mLoadFinished; + } + + @Override + public List<Recording> getRecordings() { + if (!mLoadFinished) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(mRecordings); + } + + @Override + public List<Recording> getFinishedRecordings() { + return getRecordingsWithState(Recording.STATE_RECORDING_FINISHED); + } + + @Override + public List<Recording> getStartedRecordings() { + return getRecordingsWithState(Recording.STATE_RECORDING_IN_PROGRESS); + } + + @Override + public List<Recording> getScheduledRecordings() { + return getRecordingsWithState(Recording.STATE_RECORDING_NOT_STARTED); + } + + private List<Recording> getRecordingsWithState(@RecordingState int state) { + List<Recording> result = new ArrayList<>(); + for (Recording r : mRecordings) { + if (r.getState() == state) { + result.add(r); + } + } + return result; + } + + @Override + public List<SeasonRecording> getSeasonRecordings() { + // If we return dummy data here, we can implement UI part independently. + return Collections.emptyList(); + } + + @Override + public long getNextScheduledStartTimeAfter(long startTime) { + return getNextStartTimeAfter(mRecordings, startTime); + } + + @VisibleForTesting + static long getNextStartTimeAfter(List<Recording> recordings, long startTime) { + int start = 0; + int end = recordings.size() - 1; + while (start <= end) { + int mid = (start + end) / 2; + if (recordings.get(mid).getStartTimeMs() <= startTime) { + start = mid + 1; + } else { + end = mid - 1; + } + } + return start < recordings.size() ? recordings.get(start).getStartTimeMs() + : NEXT_START_TIME_NOT_FOUND; + } + + @Override + public List<Recording> getRecordingsThatOverlapWith(Range<Long> period) { + List<Recording> result = new ArrayList<>(); + for (Recording r : mRecordings) { + if (r.isOverLapping(period)) { + result.add(r); + } + } + return result; + } + + @Override + public void addRecording(Recording recording) { } + + @Override + public void addSeasonRecording(SeasonRecording seasonRecording) { } + + @Override + public void removeRecording(Recording recording) { } + + @Override + public void removeSeasonSchedule(SeasonRecording seasonSchedule) { } + + @Override + public void updateRecording(Recording r) { } +} diff --git a/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java b/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java new file mode 100644 index 00000000..5dbdaac3 --- /dev/null +++ b/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java @@ -0,0 +1,139 @@ +/* + * 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.content.Context; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.util.Range; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A DVR Data manager that stores values in memory suitable for testing. + */ +@VisibleForTesting // TODO(DVR): move to testing dir. +public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager { + private final Map<Long, Recording> mRecordings = new HashMap<>(); + private List<SeasonRecording> mSeasonSchedule = new ArrayList<>(); + + DvrDataManagerInMemoryImpl(Context context) { + super(context); + } + + @Override + public boolean isInitialized() { + return true; + } + + @Override + public List<Recording> getRecordings() { + return new ArrayList(mRecordings.values()); + } + + @Override + public List<Recording> getFinishedRecordings() { + //TODO filter + return new ArrayList(mRecordings.values()); + } + + @Override + public List<Recording> getStartedRecordings() { + return null; + } + + @Override + public List<Recording> getScheduledRecordings() { + //TODO filter + return new ArrayList(mRecordings.values()); + } + + @Override + public List<SeasonRecording> getSeasonRecordings() { + return mSeasonSchedule; + } + + @Override + public long getNextScheduledStartTimeAfter(long startTime) { + + List<Recording> temp = getScheduledRecordings(); + Collections.sort(temp, Recording.START_TIME_COMPARATOR); + for (Recording r : temp) { + if (r.getStartTimeMs() > startTime) { + return r.getStartTimeMs(); + } + } + return DvrDataManager.NEXT_START_TIME_NOT_FOUND; + } + + @Override + public List<Recording> getRecordingsThatOverlapWith(Range<Long> period) { + List<Recording> temp = getRecordings(); + List<Recording> result = new ArrayList<>(); + for (Recording r : temp) { + if (r.isOverLapping(period)) { + result.add(r); + } + } + return result; + } + + /** + * Add a new recording. + */ + @Override + public void addRecording(Recording recording) { + mRecordings.put(recording.getId(), recording); + notifyRecordingAdded(recording); + } + + @Override + public void addSeasonRecording(SeasonRecording seasonRecording) { + mSeasonSchedule.add(seasonRecording); + } + + @Override + public void removeRecording(Recording recording) { + mRecordings.remove(recording.getId()); + notifyRecordingRemoved(recording); + } + + @Override + public void removeSeasonSchedule(SeasonRecording seasonSchedule) { + mSeasonSchedule.remove(seasonSchedule); + } + + @Override + public void updateRecording(Recording r) { + long id = r.getId(); + if (mRecordings.containsKey(id)) { + mRecordings.put(id, r); + notifyRecordingStatusChanged(r); + } else { + throw new IllegalArgumentException("Recording not found:" + r); + } + } + + @Nullable + public Recording getRecording(long id) { + return mRecordings.get(id); + } +} diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java new file mode 100644 index 00000000..35b367ba --- /dev/null +++ b/src/com/android/tv/dvr/DvrManager.java @@ -0,0 +1,104 @@ +/* + * 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.content.Context; +import android.util.Log; + +import com.android.tv.ApplicationSingletons; +import com.android.tv.Features; +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.Program; +import com.android.tv.util.SoftPreconditions; +import com.android.tv.util.Utils; + +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}. + */ +public class DvrManager { + private final static String TAG = "DvrManager"; + private final WritableDvrDataManager mDataManager; + private final ChannelDataManager mChannelDataManager; + + public DvrManager(Context context) { + SoftPreconditions.checkFeatureEnabled(context, Features.DVR, TAG); + ApplicationSingletons appSingletons = TvApplication.getSingletons(context); + mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager(); + mChannelDataManager = appSingletons.getChannelDataManager(); + } + + /** + * Adds a recording schedule for {@code program}. + */ + public void addSchedule(Program program) { + Log.i(TAG, "Adding scheduled recording of " + program); + //TODO: handle error cases + Channel c = mChannelDataManager.getChannel(program.getChannelId()); + Recording r = Recording.builder(c, program).build(); + mDataManager.addRecording(r); + } + + /** + * Adds a recording schedule with a time range. + */ + public void addSchedule(Channel channel, long startTime, long endTime) { + Log.i(TAG, "Adding scheduled recording of channel" + channel + " starting at " + + Utils.toTimeString(startTime) + " and ending at " + Utils.toTimeString(endTime)); + //TODO: handle error cases + Recording r = Recording.builder(channel, startTime, endTime).build(); + mDataManager.addRecording(r); + } + + /** + * Adds a season recording schedule based on {@code program}. + */ + public void addSeasonSchedule(Program program) { + Log.i(TAG, "Adding season recording of " + program); + // TODO: implement + } + + /** + * Removes a scheduled recording or an existing recording. + */ + public void removeRecording(Recording recording) { + Log.i(TAG, "Removing " + recording); + mDataManager.removeRecording(recording); + } + + /** + * Checks whether {@code program} can be recorded without any conflict. If there is any + * conflict, {@code outConflictRecordings} will be filled. + */ + public boolean canAddSchedule(Program program, List<Recording> outConflictRecordings) { + // TODO: implement + return true; + } + + /** + * Checks whether {@code channel} can be tuned without any conflict with existing recordings + * in progress. If there is any conflict, {@code outConflictRecordings} will be filled. + */ + public boolean canTuneTo(Channel channel, List<Recording> outConflictRecordings) { + // TODO: implement + return true; + } +} diff --git a/src/com/android/tv/dvr/DvrRecordingService.java b/src/com/android/tv/dvr/DvrRecordingService.java new file mode 100644 index 00000000..d7a044ab --- /dev/null +++ b/src/com/android/tv/dvr/DvrRecordingService.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; + +import android.app.AlarmManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Binder; +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.util.Clock; +import com.android.tv.util.SoftPreconditions; + +/** + * DVR Scheduler service. + * + * <p> This service is responsible for: + * <ul> + * <li>Send record commands to TV inputs</li> + * <li>Wake up at proper timing for recording</li> + * <li>Deconflict schedule, handling overlapping times etc.</li> + * <li> + * + * </ul> + * + * <p>The service does not stop it self. + */ +public class DvrRecordingService extends Service { + private static final String TAG = "DvrRecordingService"; + private static final boolean DEBUG = false; + public static void startService(Context context) { + Intent dvrSchedulerIntent = new Intent(context, DvrRecordingService.class); + context.startService(dvrSchedulerIntent); + } + + private DvrSessionManager mSessionManager; + private WritableDvrDataManager mDataManager; + + /** + * Class for clients to access. Because we know this service always + * runs in the same process as its clients, we don't need to deal with + * IPC. + */ + public class SchedulerBinder extends Binder { + Scheduler getScheduler() { + return mScheduler; + } + } + + private final IBinder mBinder = new SchedulerBinder(); + + private Scheduler mScheduler; + + @Override + public void onCreate() { + if (DEBUG) Log.d(TAG, "onCreate"); + super.onCreate(); + SoftPreconditions.checkFeatureEnabled(this, Features.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, + alarmManager); + } + mDataManager.addListener(mScheduler); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (DEBUG) Log.d(TAG, "onStartCommand (" + intent + "," + flags + "," + startId + ")"); + mScheduler.update(); + return START_STICKY; + } + + @Override + public void onDestroy() { + if (DEBUG) Log.d(TAG, "onDestroy"); + mDataManager.removeListener(mScheduler); + mScheduler = null; + super.onDestroy(); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @VisibleForTesting + void setScheduler(Scheduler scheduler) { + Log.i(TAG, "Setting scheduler for tests to " + scheduler); + mScheduler = scheduler; + } +} diff --git a/src/com/android/tv/dvr/DvrSessionManager.java b/src/com/android/tv/dvr/DvrSessionManager.java new file mode 100644 index 00000000..815dfeb1 --- /dev/null +++ b/src/com/android/tv/dvr/DvrSessionManager.java @@ -0,0 +1,51 @@ +/* + * 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.content.Context; + +import com.android.tv.Features; +import com.android.tv.common.dvr.DvrSessionClient; +import com.android.tv.data.Channel; +import com.android.tv.util.SoftPreconditions; + +/** + * Manages Dvr Sessions. + * Responsible for: + * <ul> + * <li>Manage DvrSession</li> + * <li>Manage capabilities (conflict)</li> + * </ul> + */ +public class DvrSessionManager { + private final static String TAG = "DvrSessionManager"; + + public DvrSessionManager(Context context) { + SoftPreconditions.checkFeatureEnabled(context, Features.DVR, TAG); + } + + public DvrSessionClient acquireDvrSession(String inputId, Channel channel) { + return null; + } + + public boolean canAcquireDvrSession(String inputId, Channel channel) { + return false; + } + + public void releaseDvrSession(DvrSessionClient session) { + } +} diff --git a/src/com/android/tv/dvr/DvrStartRecordingReceiver.java b/src/com/android/tv/dvr/DvrStartRecordingReceiver.java new file mode 100644 index 00000000..3649ad1e --- /dev/null +++ b/src/com/android/tv/dvr/DvrStartRecordingReceiver.java @@ -0,0 +1,31 @@ +/* + * 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.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * Signals the DVR to start recording shows <i>soon</i>. + */ +public class DvrStartRecordingReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + DvrRecordingService.startService(context); + } +} diff --git a/src/com/android/tv/dvr/Recording.java b/src/com/android/tv/dvr/Recording.java new file mode 100644 index 00000000..9695d268 --- /dev/null +++ b/src/com/android/tv/dvr/Recording.java @@ -0,0 +1,410 @@ +/* + * 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.database.Cursor; +import android.net.Uri; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.util.Range; + +import com.android.tv.data.Channel; +import com.android.tv.data.Program; +import com.android.tv.dvr.provider.DvrContract; +import com.android.tv.util.SoftPreconditions; +import com.android.tv.util.Utils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * A data class for one recording contents. + */ +@VisibleForTesting +public final class Recording { + private static final String TAG = "Recording"; + + public static final Comparator<Recording> START_TIME_COMPARATOR = new Comparator<Recording>() { + @Override + public int compare(Recording lhs, Recording rhs) { + return Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs); + } + }; + + public static Builder builder(Channel c, Program p) { + return new Builder() + .setChannel(c) + .setStartTime(p.getStartTimeUtcMillis()) + .setEndTime(p.getEndTimeUtcMillis()) + .setPrograms(Collections.singletonList(p)) + .setType(TYPE_PROGRAM); + } + + public static Builder builder(Channel c, long startTime, long endTime) { + return new Builder() + .setChannel(c) + .setStartTime(startTime) + .setEndTime(endTime); + } + + public static final class Builder { + private long mId; + private Uri mUri; + private Channel mChannel; + private List<Program> mPrograms; + private @RecordingType int mType; + private long mStartTime; + private long mEndTime; + private long mSize; + private @RecordingState int mState; + private SeasonRecording mParentSeasonRecording; + + private Builder() { } + + public Builder setId(long id) { + mId = id; + return this; + } + + public Builder setUri(Uri uri) { + mUri = uri; + return this; + } + + private Builder setChannel(Channel channel) { + mChannel = channel; + return this; + } + + public Builder setPrograms(List<Program> programs) { + mPrograms = programs; + return this; + } + + private Builder setType(@RecordingType int type) { + mType = type; + return this; + } + + public Builder setStartTime(long startTime) { + mStartTime = startTime; + return this; + } + + public Builder setEndTime(long endTime) { + mEndTime = endTime; + return this; + } + + public Builder setSize(long size) { + mSize = size; + return this; + } + + public Builder setState(@RecordingState int state) { + mState = state; + return this; + } + + public Builder setParentSeasonRecording(SeasonRecording parentSeasonRecording) { + mParentSeasonRecording = parentSeasonRecording; + return this; + } + + public Recording build() { + return new Recording(mId, mUri, mChannel, mPrograms, mType, mStartTime, mEndTime, mSize, + mState, mParentSeasonRecording); + } + } + + /** + * Creates {@link Builder} object from the given original {@code Recording}. + */ + public static Builder buildFrom(Recording orig) { + return new Builder() + .setId(orig.mId) + .setChannel(orig.mChannel) + .setEndTime(orig.mEndTimeMs) + .setParentSeasonRecording(orig.mParentSeasonRecording) + .setPrograms(orig.mPrograms) + .setSize(orig.mMediaSize) + .setStartTime(orig.mStartTimeMs) + .setState(orig.mState) + .setUri(orig.mUri); + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_RECORDING_NOT_STARTED, STATE_RECORDING_IN_PROGRESS, + STATE_RECORDING_UNEXPECTEDLY_STOPPED, STATE_RECORDING_FINISHED, STATE_RECORDING_FAILED}) + public @interface RecordingState {} + public static final int STATE_RECORDING_NOT_STARTED = 0; + public static final int STATE_RECORDING_IN_PROGRESS = 1; + public static final int STATE_RECORDING_UNEXPECTEDLY_STOPPED = 2; + public static final int STATE_RECORDING_FINISHED = 3; + public static final int STATE_RECORDING_FAILED = 4; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_TIMED, TYPE_PROGRAM}) + public @interface RecordingType {} + /** + * Record with given time range. + */ + private static final int TYPE_TIMED = 1; + /** + * Record with a given program. + */ + private static final int TYPE_PROGRAM = 2; + + @RecordingType private final int mType; + + /** + * Use this projection if you want to create {@link Recording} object using {@link #fromCursor}. + */ + public static final String[] PROJECTION = { + // Columns must match what is read in Recording.fromCursor() + DvrContract.Recordings._ID, + DvrContract.Recordings.COLUMN_TYPE, + DvrContract.Recordings.COLUMN_URI, + DvrContract.Recordings.COLUMN_CHANNEL_ID, + DvrContract.Recordings.COLUMN_START_TIME_UTC_MILLIS, + DvrContract.Recordings.COLUMN_END_TIME_UTC_MILLIS, + DvrContract.Recordings.COLUMN_MEDIA_SIZE, + DvrContract.Recordings.COLUMN_STATE + }; + + /** + * The ID internal to Live TV + */ + private final long mId; + + /** + * 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}. + */ + @Nullable + private final Uri mUri; + + /** + * Note: mChannel and mPrograms should be loaded from a separate storage not + * from TvProvider, because info from TvProvider can be removed or edited later. + */ + @NonNull + private final Channel mChannel; + /** + * Recorded program info. Its size is usually 1. But, when a channel is recorded by given time + * range, multiple programs can be recorded in one recording. + */ + @NonNull + private final List<Program> mPrograms; + + private final long mStartTimeMs; + private final long mEndTimeMs; + private final long mMediaSize; + @RecordingState private final int mState; + + private final SeasonRecording mParentSeasonRecording; + + + private Recording(long id, Uri uri, Channel channel, List<Program> programs, + @RecordingType int type, long startTime, long endTime, long size, + @RecordingState int state, SeasonRecording parentSeasonRecording) { + mId = id; + mUri = uri; + mChannel = channel; + mPrograms = programs == null ? Collections.EMPTY_LIST : new ArrayList<>(programs); + mType = type; + mStartTimeMs = startTime; + mEndTimeMs = endTime; + mMediaSize = size; + mState = state; + mParentSeasonRecording = parentSeasonRecording; + } + + /** + * Returns recording schedule type. The possible types are {@link #TYPE_PROGRAM} and + * {@link #TYPE_TIMED}. + */ + @RecordingType + public int getType() { + return mType; + } + + /** + * Returns {@link android.net.Uri} representing the recording. + */ + public Uri getUri() { + return mUri; + } + + /** + * Returns recorded {@link Channel}. + */ + public Channel getChannel() { + return mChannel; + } + + /** + * Returns a list of recorded {@link Program}. + */ + public List<Program> getPrograms() { + return mPrograms; + } + + /** + * Returns started time. + */ + public long getStartTimeMs() { + return mStartTimeMs; + } + + /** + * Returns ended time. + */ + public long getEndTimeMs() { + return mEndTimeMs; + } + + /** + * Returns duration. + */ + public long getDuration() { + return mEndTimeMs - mStartTimeMs; + } + + /** + * Returns file size which this record consumes. + */ + public long getSize() { + return mMediaSize; + } + + /** + * Returns the state. The possible states are {@link #STATE_RECORDING_FINISHED}, + * {@link #STATE_RECORDING_IN_PROGRESS} and {@link #STATE_RECORDING_UNEXPECTEDLY_STOPPED}. + */ + @RecordingState public int getState() { + return mState; + } + + /** + * Returns {@link SeasonRecording} including this schedule. + */ + public SeasonRecording getParentSeasonRecording() { + return mParentSeasonRecording; + } + + public long getId() { + return mId; + } + + /** + * Creates {@link Recording} object from the given {@link Cursor}. + */ + public static Recording fromCursor(Cursor c, Channel channel, List<Program> programs) { + Builder builder = new Builder(); + int index = -1; + builder.setId(c.getLong(++index)); + builder.setType(recordingType(c.getString(++index))); + String uri = c.getString(++index); + if (uri != null) { + builder.setUri(Uri.parse(uri)); + } + // Skip channel. + ++index; + builder.setStartTime(c.getLong(++index)); + builder.setEndTime(c.getLong(++index)); + builder.setSize(c.getLong(++index)); + builder.setState(recordingState(c.getString(++index))); + builder.setChannel(channel); + builder.setPrograms(programs); + return builder.build(); + } + + /** + * Converts a string to a @RecordingType int, defaulting to {@link #TYPE_TIMED}. + */ + private static @RecordingType int recordingType(String type) { + int t; + try { + t = Integer.valueOf(type); + } catch (NullPointerException | NumberFormatException e) { + SoftPreconditions.checkArgument(false, TAG, "Unknown recording type " + type); + return TYPE_TIMED; + } + switch (t) { + case TYPE_TIMED: + return TYPE_TIMED; + case TYPE_PROGRAM: + return TYPE_PROGRAM; + default: + SoftPreconditions.checkArgument(false, TAG, "Unknown recording type " + type); + return TYPE_TIMED; + } + } + + /** + * Converts a string to a @RecordingState int, defaulting to + * {@link #STATE_RECORDING_NOT_STARTED}. + */ + private static @RecordingState int recordingState(String state) { + int s; + try { + s = Integer.valueOf(state); + } catch (NullPointerException | NumberFormatException e) { + SoftPreconditions.checkArgument(false, TAG, "Unknown recording state" + state); + return STATE_RECORDING_NOT_STARTED; + } + switch (s) { + case STATE_RECORDING_NOT_STARTED: + return STATE_RECORDING_NOT_STARTED; + case STATE_RECORDING_IN_PROGRESS: + return STATE_RECORDING_IN_PROGRESS; + case STATE_RECORDING_FINISHED: + return STATE_RECORDING_FINISHED; + case STATE_RECORDING_UNEXPECTEDLY_STOPPED: + return STATE_RECORDING_UNEXPECTEDLY_STOPPED; + case STATE_RECORDING_FAILED: + return STATE_RECORDING_FAILED; + default: + SoftPreconditions.checkArgument(false, TAG, "Unknown recording state" + state); + return STATE_RECORDING_NOT_STARTED; + } + } + + /** + * Checks if the {@code period} overlaps with the recording time. + */ + public boolean isOverLapping(Range<Long> period) { + return mStartTimeMs <= period.getUpper() && mEndTimeMs >= period.getLower(); + } + + @Override + public String toString() { + return "Recording[" + mId + + "]" + + "(startTime=" + Utils.toIsoDateTimeString(mStartTimeMs) + + ",endTime=" + Utils.toIsoDateTimeString(mEndTimeMs) + + ",state=" + mState + + ")"; + } +} diff --git a/src/com/android/tv/dvr/RecordingTask.java b/src/com/android/tv/dvr/RecordingTask.java new file mode 100644 index 00000000..e4da5f19 --- /dev/null +++ b/src/com/android/tv/dvr/RecordingTask.java @@ -0,0 +1,133 @@ +/* + * 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.net.Uri; +import android.support.annotation.VisibleForTesting; +import android.util.Log; + +import com.android.tv.common.dvr.DvrSessionClient; +import com.android.tv.data.Channel; +import com.android.tv.util.Clock; + +import java.util.concurrent.TimeUnit; + +/** + * A runnable that actually starts on stop a recording at the right time. + */ +class RecordingTask extends DvrSessionClient.Callback implements Runnable { + private static final String TAG = "RecordingTask"; + private static final boolean DEBUG = false; + + @VisibleForTesting + static long MS_BEFORE_START = TimeUnit.SECONDS.toMillis(5); + @VisibleForTesting + static long MS_AFTER_END = TimeUnit.SECONDS.toMillis(5); + private final DvrSessionManager mSessionManager; + private final WritableDvrDataManager mDataManager; + private final Clock mClock; + private Recording mRecording; + + RecordingTask(Recording recording, DvrSessionManager sessionManager, + WritableDvrDataManager dataManager, Clock clock) { + mRecording = recording; + 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); + + //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; + } + 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); + } + } + + @Override + public void onRecordStarted(Uri mediaUri) { + if (DEBUG) Log.d(TAG, "onRecordStarted " + mediaUri); + super.onRecordStarted(mediaUri); + updateRecording(Recording.buildFrom(mRecording) + .setState(Recording.STATE_RECORDING_IN_PROGRESS) + .setUri(mediaUri) + .build()); + } + + @Override + public void onRecordStopped(Uri mediaUri, @DvrSessionClient.RecordStopReason int reason) { + if (DEBUG) Log.d(TAG, "onRecordStopped " + mediaUri + " reason " + reason); + super.onRecordStopped(mediaUri, reason); + + //TODO need a success reason. + switch (reason){ + default: + updateRecording(Recording.buildFrom(mRecording) + .setState(Recording.STATE_RECORDING_FAILED) + .build()); + } + } + + + private void updateRecordingState(@Recording.RecordingState int state) { + updateRecording(Recording.buildFrom(mRecording).setState(state).build()); + } + + @VisibleForTesting static Uri getIdAsMediaUri(Recording recording) { + // TODO define the URI format + return new Uri.Builder().appendPath(String.valueOf(recording.getId())).build(); + } + + private void updateRecording(Recording updatedRecording) { + if (DEBUG) Log.d(TAG, "updateRecording " + updatedRecording); + mRecording = updatedRecording; + mDataManager.updateRecording(mRecording); + } + + @Override + public String toString() { + return getClass().getName() + "(" + mRecording + ")"; + } +} diff --git a/src/com/android/tv/dvr/Scheduler.java b/src/com/android/tv/dvr/Scheduler.java new file mode 100644 index 00000000..0787adc3 --- /dev/null +++ b/src/com/android/tv/dvr/Scheduler.java @@ -0,0 +1,169 @@ +/* + * 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.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +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; + +/** + * The core class to manage schedule and run actual recording. + */ +@VisibleForTesting +public class Scheduler implements DvrDataManager.Listener { + private static final String TAG = "Scheduler"; + private static final boolean DEBUG = false; + + /** + * Wraps a RecordingTask removing it from {@link #mPendingRecordings} when it is done. + */ + private final class TaskWrapper extends FutureTask<Void> { + private final long mId; + + TaskWrapper(Recording recording) { + super(new RecordingTask(recording, mSessionManager, mDataManager, mClock), null); + mId = recording.getId(); + } + + @Override + public void done() { + if (DEBUG) Log.d(TAG, "done " + mId); + mPendingRecordings.remove(mId); + super.done(); + } + } + + 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, + AlarmManager alarmManager) { + mSessionManager = sessionManager; + mDataManager = dataManager; + mContext = context; + mClock = clock; + mAlarmManager = alarmManager; + } + + private void updatePendingRecordings() { + List<Recording> recordings = mDataManager.getRecordingsThatOverlapWith( + new Range(mClock.currentTimeMillis(), + mClock.currentTimeMillis() + SOON_DURATION_IN_MS)); + // TODO(DVR): handle removing and updating exiting recordings. + for (Recording r : recordings) { + scheduleRecordingSoon(r); + } + } + + /** + * Start recording that will happen soon, and set the next alarm time. + */ + public void update() { + if (DEBUG) Log.d(TAG, "update"); + updatePendingRecordings(); + updateNextAlarm(); + } + + + @Override + public void onRecordingAdded(Recording recording) { + if (DEBUG) Log.d(TAG, "added " + recording); + if (startsWithin(recording, SOON_DURATION_IN_MS)) { + scheduleRecordingSoon(recording); + } else { + updateNextAlarm(); + } + } + + @Override + public void onRecordingRemoved(Recording recording) { + long id = recording.getId(); + TaskWrapper task = mPendingRecordings.get(id); + if (task != null) { + task.cancel(true); + mPendingRecordings.remove(id); + } else { + updateNextAlarm(); + } + } + + @Override + public void onRecordingStatusChanged(Recording recording) { + //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); + } + + private void updateNextAlarm() { + long lastStartTimePending = getLastStartTimePending(); + long nextStartTime = mDataManager.getNextScheduledStartTimeAfter(lastStartTimePending); + if (nextStartTime != DvrDataManager.NEXT_START_TIME_NOT_FOUND) { + 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); + //This will cancel the previous alarm. + mAlarmManager.set(AlarmManager.RTC_WAKEUP, wakeAt, mAlarmIntent); + } else { + if (DEBUG) Log.d(TAG, "No future recording, alarm not set"); + } + } + + private long getLastStartTimePending() { + // TODO(DVR): implement + return mClock.currentTimeMillis(); + } + + @VisibleForTesting + boolean startsWithin(Recording recording, long durationInMs) { + return mClock.currentTimeMillis() >= recording.getStartTimeMs() - durationInMs; + } +} diff --git a/src/com/android/tv/dvr/SeasonRecording.java b/src/com/android/tv/dvr/SeasonRecording.java new file mode 100644 index 00000000..074ef017 --- /dev/null +++ b/src/com/android/tv/dvr/SeasonRecording.java @@ -0,0 +1,35 @@ +/* + * 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 java.util.List; + +/** + * A data class for one recorded contents. + */ +public class SeasonRecording { + private static final String TAG = "Recording"; + + /** + * Constant for all season. + */ + private static final int ALL_SEASON = -1; + + private List<Recording> mSchedule; + private String mTitle; + private int mSeasonNumber; +} diff --git a/src/com/android/tv/dvr/WritableDvrDataManager.java b/src/com/android/tv/dvr/WritableDvrDataManager.java new file mode 100644 index 00000000..d1ba702e --- /dev/null +++ b/src/com/android/tv/dvr/WritableDvrDataManager.java @@ -0,0 +1,50 @@ +/* + * 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; + +/** + * 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. + */ +interface WritableDvrDataManager extends DvrDataManager { + /** + * Add a new recording. + */ + void addRecording(Recording recording); + + /** + * Add a season recording/ + */ + void addSeasonRecording(SeasonRecording seasonRecording); + + /** + * Remove a recording. + */ + void removeRecording(Recording Recording); + + /** + * Remove a season schedule. + */ + void removeSeasonSchedule(SeasonRecording seasonSchedule); + + /** + * Update an existing recording. + */ + void updateRecording(Recording r); +} diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java new file mode 100644 index 00000000..55748937 --- /dev/null +++ b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java @@ -0,0 +1,148 @@ +/* + * 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.provider; + +import android.content.Context; +import android.database.Cursor; +import android.os.AsyncTask; + +import com.android.tv.data.Channel; +import com.android.tv.data.Program; +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 com.android.tv.util.NamedThreadFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * {@link AsyncTask} that defaults to executing on its own single threaded Executor Service. + */ +public abstract class AsyncDvrDbTask<Params, Progress, Result> + extends AsyncTask<Params, Progress, Result> { + private static final NamedThreadFactory THREAD_FACTORY = new NamedThreadFactory( + AsyncDvrDbTask.class.getSimpleName()); + private static final ExecutorService DB_EXECUTOR = Executors + .newSingleThreadExecutor(THREAD_FACTORY); + + private static DvrDatabaseHelper sDbHelper; + + private static synchronized DvrDatabaseHelper initializeDbHelper(Context context) { + if (sDbHelper == null) { + sDbHelper = new DvrDatabaseHelper(context.getApplicationContext()); + } + return sDbHelper; + } + + final Context mContext; + + private AsyncDvrDbTask(Context context) { + mContext = context; + } + + /** + * Execute the task on the {@link #DB_EXECUTOR} thread. + */ + @SafeVarargs + public final void executeOnDbThread(Params... params) { + executeOnExecutor(DB_EXECUTOR, params); + } + + public abstract static class AsyncDvrQueryTask + extends AsyncDvrDbTask<Void, Void, List<Recording>> { + public AsyncDvrQueryTask(Context context) { + super(context); + } + + @Override + protected List<Recording> doInBackground(Void... params) { + initializeDbHelper(mContext); + + if (isCancelled()) { + return null; + } + // Read Channels Table. + Map<Long, Channel> channelMap = new HashMap<>(); + try (Cursor c = sDbHelper.query(DvrChannels.TABLE_NAME, Channel.PROJECTION_DVR)) { + while (c.moveToNext() && !isCancelled()) { + Channel channel = Channel.fromDvrCursor(c); + channelMap.put(channel.getDvrId(), channel); + } + } + + if (isCancelled()) { + return null; + } + // Read Programs Table. + Map<Long, Program> programMap = new HashMap<>(); + try (Cursor c = sDbHelper.query(DvrPrograms.TABLE_NAME, Program.PROJECTION_DVR)) { + while (c.moveToNext() && !isCancelled()) { + Program program = Program.fromDvrCursor(c); + programMap.put(program.getDvrId(), program); + } + } + + if (isCancelled()) { + return null; + } + // Read Mapping Table. + Map<Long, List<Long>> recordingToProgramMap = new HashMap<>(); + try (Cursor c = sDbHelper.query(RecordingToPrograms.TABLE_NAME, new String[] { + RecordingToPrograms.COLUMN_RECORDING_ID, + RecordingToPrograms.COLUMN_PROGRAM_ID})) { + while (c.moveToNext() && !isCancelled()) { + long recordingId = c.getLong(0); + List<Long> programList = recordingToProgramMap.get(recordingId); + if (programList == null) { + programList = new ArrayList<>(); + } + programList.add(c.getLong(1)); + } + } + + if (isCancelled()) { + return null; + } + List<Recording> recordings = new ArrayList<>(); + try (Cursor c = sDbHelper.query(Recordings.TABLE_NAME, Recording.PROJECTION)) { + int idIndex = c.getColumnIndex(Recordings._ID); + int channelIndex = c.getColumnIndex(Recordings.COLUMN_CHANNEL_ID); + while (c.moveToNext() && !isCancelled()) { + Channel channel = channelMap.get(c.getLong(channelIndex)); + List<Program> programs = null; + long recordingId = c.getLong(idIndex); + List<Long> programIds = recordingToProgramMap.get(recordingId); + if (programIds != null) { + programs = new ArrayList<>(); + for (long programId : programIds) { + programs.add(programMap.get(programId)); + } + } + recordings.add(Recording.fromCursor(c, channel, programs)); + } + } + return recordings; + } + } +} diff --git a/src/com/android/tv/dvr/provider/DvrContract.java b/src/com/android/tv/dvr/provider/DvrContract.java new file mode 100644 index 00000000..650de2e4 --- /dev/null +++ b/src/com/android/tv/dvr/provider/DvrContract.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.provider; + +import android.provider.BaseColumns; + +/** + * The contract between the DVR provider and applications. Contains definitions for the supported + * columns. It's for the internal use in Live TV. + */ +public final class DvrContract { + /** Column definition for Recording table. */ + public static final class Recordings implements BaseColumns { + /** The table name. */ + public static final String TABLE_NAME = "recording"; + + /** The recording type for program recording. */ + public static final String TYPE_PROGRAM = "TYPE_PROGRAM"; + + /** The recording type for timed recording. */ + public static final String TYPE_TIMED = "TYPE_TIMED"; + + /** The recording type for season recording. */ + public static final String TYPE_SEASON_RECORDING = "TYPE_SEASON_RECORDING"; + + /** The recording has not been started yet. */ + public static final String STATE_RECORDING_NOT_STARTED = "STATE_RECORDING_NOT_STARTED"; + + /** The recording is in progress. */ + public static final String STATE_RECORDING_IN_PROGRESS = "STATE_RECORDING_IN_PROGRESS"; + + /** The recording was unexpectedly stopped. */ + public static final String STATE_RECORDING_UNEXPECTEDLY_STOPPED = + "STATE_RECORDING_UNEXPECTEDLY_STOPPED"; + + /** The recording is finished. */ + public static final String STATE_RECORDING_FINISHED = "STATE_RECORDING_FINISHED"; + + /** + * The type of this recording. + * + * <p>This value should be one of the followings: {@link #TYPE_PROGRAM}, + * {@link #TYPE_TIMED}, and {@link #TYPE_SEASON_RECORDING}. + * + * <p>This is a required field. + * + * <p>Type: String + */ + public static final String COLUMN_TYPE = "type"; + + /** + * The URI string for the recorded media. + * + * <p>This field can be null if the media is not recorded yet. + * + * <p>Type: String + */ + public static final String COLUMN_URI = "uri"; + + /** + * The ID of the channel for recording. + * + * <p>This is a required field. It's not an ID in TvProvider, but in DVR database. + * + * <p>Type: INTEGER (long) + */ + public static final String COLUMN_CHANNEL_ID = "channel_id"; + + /** + * The start time of this recording, in milliseconds since the epoch. + * + * <p>This is a required field. + * + * <p>Type: INTEGER (long) + */ + public static final String COLUMN_START_TIME_UTC_MILLIS = "start_time_utc_millis"; + + /** + * The end time of this recording, in milliseconds since the epoch. + * + * <p>This is a required field. + * + * <p>Type: INTEGER (long) + */ + public static final String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis"; + + /** + * The size of the stored media in bytes. + * + * <p>Type: INTEGER (long) + */ + public static final String COLUMN_MEDIA_SIZE = "media_size"; + + /** + * The state of this recording. + * + * <p>This value should be one of the followings: {@link #STATE_RECORDING_NOT_STARTED}, + * {@link #STATE_RECORDING_IN_PROGRESS}, {@link #STATE_RECORDING_UNEXPECTEDLY_STOPPED}, + * and {@link #STATE_RECORDING_FINISHED}. + * + * <p>This is a required field. + * + * <p>Type: String + */ + public static final String COLUMN_STATE = "state"; + } + + /** + * Column definition for channels for recording. + * + * <p>This is the subset of {@link android.media.tv.TvContract.Channels}. + */ + public static final class DvrChannels implements BaseColumns { + /** The table name. */ + public static final String TABLE_NAME = "dvr_channels"; + } + + /** + * Column definition for programs for recording. + * + * <p>This is the subset of {@link android.media.tv.TvContract.Programs}. + */ + public static final class DvrPrograms implements BaseColumns { + /** The table name. */ + public static final String TABLE_NAME = "dvr_programs"; + } + + /** Column definition for the mapping from recording to programs */ + public static final class RecordingToPrograms implements BaseColumns { + /** The table name. */ + public static final String TABLE_NAME = "recording_to_programs"; + + /** + * The ID of the recording. + * + * <p>This is a required field. + * + * <p>Type: INTEGER (long) + */ + public static final String COLUMN_RECORDING_ID = "recording_id"; + + /** + * The ID of the program. + * + * <p>This is a required field. It's not an ID in TvProvider, but in DVR database. + * + * <p>Type: INTEGER (long) + */ + public static final String COLUMN_PROGRAM_ID = "program_id"; + } +} diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java new file mode 100644 index 00000000..e9bfc340 --- /dev/null +++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java @@ -0,0 +1,124 @@ +/* + * 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.provider; + +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.util.Log; + +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; + +/** + * A data class for one recorded contents. + */ +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 String DB_NAME = "dvr.db"; + + private static final String SQL_CREATE_RECORDINGS = + "CREATE TABLE " + Recordings.TABLE_NAME + "(" + + Recordings._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Recordings.COLUMN_TYPE + " TEXT NOT NULL," + + Recordings.COLUMN_URI + " TEXT," + + Recordings.COLUMN_CHANNEL_ID + " INTEGER NOT NULL," + + Recordings.COLUMN_START_TIME_UTC_MILLIS + " INTEGER NOT NULL," + + Recordings.COLUMN_END_TIME_UTC_MILLIS + " INTEGER NOT NULL," + + Recordings.COLUMN_MEDIA_SIZE + " INTEGER," + + Recordings.COLUMN_STATE + " TEXT NOT NULL)"; + + private static final String SQL_CREATE_DVR_CHANNELS = + "CREATE TABLE " + DvrChannels.TABLE_NAME + "(" + + DvrChannels._ID + " INTEGER PRIMARY KEY AUTOINCREMENT)"; + + private static final String SQL_CREATE_DVR_PROGRAMS = + "CREATE TABLE " + DvrPrograms.TABLE_NAME + "(" + + DvrPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT)"; + + private static final String SQL_CREATE_RECORDING_PROGRAMS = + "CREATE TABLE " + RecordingToPrograms.TABLE_NAME + "(" + + RecordingToPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + RecordingToPrograms.COLUMN_RECORDING_ID + " INTEGER," + + RecordingToPrograms.COLUMN_PROGRAM_ID + " INTEGER," + + "FOREIGN KEY(" + RecordingToPrograms.COLUMN_RECORDING_ID + + ") REFERENCES " + Recordings.TABLE_NAME + "(" + Recordings._ID + + ") ON UPDATE CASCADE ON DELETE CASCADE," + + "FOREIGN KEY(" + RecordingToPrograms.COLUMN_PROGRAM_ID + + ") REFERENCES " + DvrPrograms.TABLE_NAME + "(" + DvrPrograms._ID + + ") ON UPDATE CASCADE ON DELETE CASCADE)"; + + private static final String SQL_DROP_RECORDINGS = "DROP TABLE IF EXISTS " + + Recordings.TABLE_NAME; + private static final String SQL_DROP_DVR_CHANNELS = "DROP TABLE IF EXISTS " + + DvrChannels.TABLE_NAME; + private static final String SQL_DROP_DVR_PROGRAMS = "DROP TABLE IF EXISTS " + + DvrPrograms.TABLE_NAME; + private static final String SQL_DROP_RECORDING_PROGRAMS = "DROP TABLE IF EXISTS " + + RecordingToPrograms.TABLE_NAME; + + public DvrDatabaseHelper(Context context) { + super(context.getApplicationContext(), DB_NAME, null, DATABASE_VERSION); + } + + @Override + public void onConfigure(SQLiteDatabase db) { + db.setForeignKeyConstraintsEnabled(true); + } + + @Override + public void onCreate(SQLiteDatabase db) { + if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_RECORDINGS); + db.execSQL(SQL_CREATE_RECORDINGS); + if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_DVR_CHANNELS); + db.execSQL(SQL_CREATE_DVR_CHANNELS); + if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_DVR_PROGRAMS); + db.execSQL(SQL_CREATE_DVR_PROGRAMS); + if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_RECORDING_PROGRAMS); + db.execSQL(SQL_CREATE_RECORDING_PROGRAMS); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_RECORDING_PROGRAMS); + db.execSQL(SQL_DROP_RECORDING_PROGRAMS); + if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_DVR_PROGRAMS); + db.execSQL(SQL_DROP_DVR_PROGRAMS); + if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_DVR_CHANNELS); + db.execSQL(SQL_DROP_DVR_CHANNELS); + if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_RECORDINGS); + db.execSQL(SQL_DROP_RECORDINGS); + onCreate(db); + } + + /** + * Handles the query request and returns a {@link Cursor}. + */ + public Cursor query(String tableName, String[] projections) { + SQLiteDatabase db = getReadableDatabase(); + SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); + builder.setTables(tableName); + return builder.query(db, projections, null, null, null, null, null); + } +} diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java index 57678c05..2fd2dac7 100644 --- a/src/com/android/tv/guide/ProgramItemView.java +++ b/src/com/android/tv/guide/ProgramItemView.java @@ -16,7 +16,9 @@ package com.android.tv.guide; +import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.drawable.Drawable; @@ -34,15 +36,21 @@ import android.view.View; 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.data.Channel; +import com.android.tv.data.Program; +import com.android.tv.dvr.DvrManager; import com.android.tv.guide.ProgramManager.TableEntry; import com.android.tv.util.Utils; import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; public class ProgramItemView extends TextView { @@ -51,6 +59,9 @@ public class ProgramItemView extends TextView { private static final long FOCUS_UPDATE_FREQUENCY = TimeUnit.SECONDS.toMillis(1); private static final int MAX_PROGRESS = 10000; // From android.widget.ProgressBar.MAX_VALUE + private static final int ACTION_RECORD_PROGRAM = 100; + private static final int ACTION_RECORD_SEASON = 101; + // State indicating the focused program is the current program private static final int[] STATE_CURRENT_PROGRAM = { R.attr.state_current_program }; @@ -77,8 +88,8 @@ public class ProgramItemView extends TextView { @Override public void onClick(View view) { TableEntry entry = ((ProgramItemView) view).mTableEntry; - Tracker tracker = ((TvApplication) view.getContext().getApplicationContext()) - .getTracker(); + ApplicationSingletons singletons = TvApplication.getSingletons(view.getContext()); + Tracker tracker = singletons.getTracker(); tracker.sendEpgItemClicked(); if (entry.isCurrentProgram()) { final MainActivity tvActivity = (MainActivity) view.getContext(); @@ -92,6 +103,37 @@ public class ProgramItemView extends TextView { } }, 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); + 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(); } } }; diff --git a/src/com/android/tv/menu/AppLinkCardView.java b/src/com/android/tv/menu/AppLinkCardView.java index 54564403..60a4481e 100644 --- a/src/com/android/tv/menu/AppLinkCardView.java +++ b/src/com/android/tv/menu/AppLinkCardView.java @@ -246,13 +246,15 @@ public class AppLinkCardView extends BaseCardView<Channel> implements Channel.Lo // 5) Application icon, and 6) default image. private void setCardImageWithBanner(ApplicationInfo appInfo) { Drawable banner = null; - try { - banner = mPackageManager.getActivityBanner(mIntent); - if (banner == null) { - banner = mPackageManager.getActivityIcon(mIntent); + if (mIntent != null) { + try { + banner = mPackageManager.getActivityBanner(mIntent); + if (banner == null) { + banner = mPackageManager.getActivityIcon(mIntent); + } + } catch (PackageManager.NameNotFoundException e) { + // do nothing. } - } catch (PackageManager.NameNotFoundException e) { - // do nothing. } if (banner == null && appInfo != null) { diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java index 7a1eacf3..6cbd67ce 100644 --- a/src/com/android/tv/menu/ChannelsRowAdapter.java +++ b/src/com/android/tv/menu/ChannelsRowAdapter.java @@ -88,7 +88,7 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel> public ChannelsRowAdapter(Context context, Recommender recommender, int minCount, int maxCount) { super(context); - mTracker = ((TvApplication) context.getApplicationContext()).getTracker(); + mTracker = TvApplication.getSingletons(context).getTracker(); mContext = context; mRecommender = recommender; mMinCount = minCount; diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java index 1f33bd67..613e0d62 100644 --- a/src/com/android/tv/menu/Menu.java +++ b/src/com/android/tv/menu/Menu.java @@ -33,12 +33,12 @@ 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.TvCommonUtils; import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; import com.android.tv.menu.MenuRowFactory.PartnerRow; import com.android.tv.menu.MenuRowFactory.PipOptionsRow; import com.android.tv.menu.MenuRowFactory.TvOptionsRow; -import com.android.tv.util.Utils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -122,7 +122,7 @@ public class Menu { public Menu(Context context, IMenuView menuView, MenuRowFactory menuRowFactory, OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) { mMenuView = menuView; - mTracker = ((TvApplication) context.getApplicationContext()).getTracker(); + mTracker = TvApplication.getSingletons(context).getTracker(); Resources res = context.getResources(); mShowDurationMillis = res.getInteger(R.integer.menu_show_duration); mOnMenuVisibilityChangeListener = onMenuVisibilityChangeListener; @@ -303,7 +303,7 @@ public class Menu { @VisibleForTesting void disableAnimationForTest() { - if (!Utils.isRunningInTest()) { + if (!TvCommonUtils.isRunningInTest()) { throw new RuntimeException("Animation may only be enabled/disabled during tests."); } mAnimationDisabledForTest = true; diff --git a/src/com/android/tv/menu/OptionsRowAdapter.java b/src/com/android/tv/menu/OptionsRowAdapter.java index df62026a..93bd0a4d 100644 --- a/src/com/android/tv/menu/OptionsRowAdapter.java +++ b/src/com/android/tv/menu/OptionsRowAdapter.java @@ -56,7 +56,7 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter< public OptionsRowAdapter(Context context) { super(context); - mTracker = ((TvApplication) context.getApplicationContext()).getTracker(); + mTracker = TvApplication.getSingletons(context).getTracker(); } /** diff --git a/src/com/android/tv/onboarding/AppOverviewFragment.java b/src/com/android/tv/onboarding/AppOverviewFragment.java index 3427b122..a2f5d768 100644 --- a/src/com/android/tv/onboarding/AppOverviewFragment.java +++ b/src/com/android/tv/onboarding/AppOverviewFragment.java @@ -16,7 +16,6 @@ package com.android.tv.onboarding; -import android.app.Fragment; import android.content.res.Resources; import android.os.Bundle; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; @@ -25,6 +24,7 @@ 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; @@ -54,7 +54,7 @@ public class AppOverviewFragment extends SetupMultiPaneFragment { } @Override - protected Fragment getContentFragment() { + protected SetupGuidedStepFragment onCreateContentFragment() { return new ContentFragment(); } @@ -77,8 +77,9 @@ public class AppOverviewFragment extends SetupMultiPaneFragment { @Override public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { - boolean hasTvInput = ((TvApplication) getActivity().getApplicationContext()) - .getTvInputManagerHelper().getTunerTvInputSize() > 0; + boolean hasTvInput = + TvApplication.getSingletons(getActivity()).getTvInputManagerHelper() + .getTunerTvInputSize() > 0; Resources res = getResources(); if (hasTvInput) { actions.add(new GuidedAction.Builder() @@ -88,12 +89,15 @@ public class AppOverviewFragment extends SetupMultiPaneFragment { R.string.app_overview_action_description_setup_source)) .build()); } - actions.add(new GuidedAction.Builder() - .id(ACTION_GET_MORE_CHANNELS) - .title(res.getString(R.string.app_overview_action_text_play_store)) - .description(res.getString(R.string.app_overview_action_description_play_store)) - .build()); - if (mAc3Supported) { + 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)) diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java index 4176a70f..3717a611 100644 --- a/src/com/android/tv/onboarding/OnboardingActivity.java +++ b/src/com/android/tv/onboarding/OnboardingActivity.java @@ -21,6 +21,7 @@ import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.media.tv.TvInputInfo; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -31,15 +32,18 @@ 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.data.ChannelDataManager; import com.android.tv.receiver.AudioCapabilitiesReceiver; import com.android.tv.util.OnboardingUtils; import com.android.tv.util.SetupUtils; import com.android.tv.util.SoftPreconditions; import com.android.tv.util.Utils; +import java.util.Locale; import java.util.concurrent.TimeUnit; public class OnboardingActivity extends SteppedSetupActivity { @@ -116,6 +120,15 @@ public class OnboardingActivity extends SteppedSetupActivity { startInitialStep(); } + @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; + } + super.onActivityResult(requestCode, resultCode, data); + } private static class OnboardingActivityHandler extends WeakHandler<OnboardingActivity> { OnboardingActivityHandler(OnboardingActivity activity) { @@ -132,9 +145,9 @@ public class OnboardingActivity extends SteppedSetupActivity { } } } - + void finishActivity() { - Intent intentForNextActivity = (Intent) getIntent().getParcelableExtra( + Intent intentForNextActivity = getIntent().getParcelableExtra( KEY_INTENT_AFTER_COMPLETION); if (intentForNextActivity != null) { startActivity(intentForNextActivity); @@ -153,22 +166,12 @@ public class OnboardingActivity extends SteppedSetupActivity { } @Override - protected boolean needsToBeAddedToBackStack() { - return false; - } - - @Override - protected boolean needsFragmentTransitionAnimation() { - return false; - } - - @Override public void executeAction(int actionId) { switch (actionId) { case WelcomeFragment.ACTION_NEXT: OnboardingUtils.setFirstRunCompleted(OnboardingActivity.this); if (!OnboardingUtils.areChannelsAvailable(OnboardingActivity.this)) { - startStep(new AppOverviewStep(this)); + startStep(new AppOverviewStep(this), false); } else { // TODO: Go to the correct step. finishActivity(); @@ -179,6 +182,9 @@ public class OnboardingActivity extends SteppedSetupActivity { } 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); } @@ -193,21 +199,40 @@ public class OnboardingActivity extends SteppedSetupActivity { } @Override - protected boolean needsToBeAddedToBackStack() { - return false; - } - - @Override public void executeAction(int actionId) { switch (actionId) { - case AppOverviewFragment.ACTION_SETUP_SOURCE: - startStep(new SetupSourcesStep(this)); + case AppOverviewFragment.ACTION_SETUP_SOURCE: { + startStep(new SetupSourcesStep(this), true); break; + } case AppOverviewFragment.ACTION_GET_MORE_CHANNELS: - // TODO: Implement this. - Toast.makeText(OnboardingActivity.this, "Not implemented yet.", - Toast.LENGTH_SHORT).show(); + 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; + } + // TODO: Add transition animation. + } else { + // TODO: Implement this. + Toast.makeText(OnboardingActivity.this, "Not implemented yet.", + Toast.LENGTH_SHORT).show(); + } break; + } } } } @@ -225,9 +250,16 @@ public class OnboardingActivity extends SteppedSetupActivity { @Override public void executeAction(int actionId) { switch (actionId) { - case SetupSourcesFragment.ACTION_DONE: - finishActivity(); + case SetupSourcesFragment.ACTION_DONE: { + ChannelDataManager manager = TvApplication.getSingletons( + OnboardingActivity.this).getChannelDataManager(); + if (manager.getChannelCount() == 0) { + finish(); + } else { + finishActivity(); + } break; + } } } } diff --git a/src/com/android/tv/onboarding/PagingIndicator.java b/src/com/android/tv/onboarding/PagingIndicator.java index 128fa996..107b00f0 100644 --- a/src/com/android/tv/onboarding/PagingIndicator.java +++ b/src/com/android/tv/onboarding/PagingIndicator.java @@ -82,7 +82,7 @@ public class PagingIndicator extends View { mUnselectingPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mSelectingPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // Initialize animations. - int duration = res.getInteger(R.integer.setup_slide_anim_duration); + int duration = res.getInteger(R.integer.setup_fragment_transition_duration); List<Animator> animators = new ArrayList<>(); animators.add(createColorAnimator(selectedColor, unselectedColor, duration, mUnselectingPaint)); diff --git a/src/com/android/tv/onboarding/SetupSourcesFragment.java b/src/com/android/tv/onboarding/SetupSourcesFragment.java index 3572a209..ad49dc23 100644 --- a/src/com/android/tv/onboarding/SetupSourcesFragment.java +++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java @@ -16,18 +16,36 @@ package com.android.tv.onboarding; -import android.app.Fragment; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.media.tv.TvInputInfo; import android.os.Bundle; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; +import android.support.v17.leanback.widget.VerticalGridView; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ViewHolder; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; +import com.android.tv.ApplicationSingletons; import com.android.tv.R; +import com.android.tv.SetupPassthroughActivity; +import com.android.tv.TvApplication; +import com.android.tv.common.TvCommonUtils; import com.android.tv.common.ui.setup.SetupGuidedStepFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; +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; /** @@ -43,11 +61,61 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { } @Override - protected Fragment getContentFragment() { - return new ContentFragment(); + protected SetupGuidedStepFragment onCreateContentFragment() { + SetupGuidedStepFragment fragment = new ContentFragment(getActivity()); + Bundle arguments = new Bundle(); + arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true); + fragment.setArguments(arguments); + return fragment; } private 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; + + private final TvInputManagerHelper mInputManager; + private final ChannelDataManager mChannelDataManager; + private final SetupUtils mSetupUtils; + private List<TvInputInfo> mInputList; + private SetupSourcesAdapter mAdapter; + private int mKnownInputStartIndex; + private boolean mShowDivider; + + ContentFragment(Context context) { + // TODO: Handle USB TV tuner differently. + 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(); + } + } + + @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; + } + @Override public Guidance onCreateGuidance(Bundle savedInstanceState) { String title = getString(R.string.setup_sources_text); @@ -57,8 +125,148 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { @Override public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { - actions.add(new GuidedAction.Builder().id(ACTION_DONE).title("Done") - .description("Return to previous page").build()); + createActionsInternal(actions); + if (!mChannelDataManager.isDbLoadFinished()) { + mChannelDataManager.addListener(new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { + mChannelDataManager.removeListener(this); + updateActions(); + } + + @Override + public void onChannelListUpdated() { } + + @Override + public void onChannelBrowsableChanged() { } + }); + } + } + + private void updateActions() { + 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()); + } + TvInputInfo input = mInputList.get(i); + String description; + int channelCount = mChannelDataManager.getChannelCountForInput(input.getId()); + if (mSetupUtils.isSetupDone(input.getId())) { + if (channelCount == 0) { + description = getResources().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); + } else { + description = getResources().getString(R.string.setup_input_new); + } + actions.add(new GuidedAction.Builder().id(ACTION_INPUT_START + i) + .title(input.loadLabel(getActivity()).toString()).description(description) + .build()); + } + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + TvInputInfo input = mInputList.get((int) action.getId() - ACTION_INPUT_START); + Intent intent = TvCommonUtils.createSetupIntent(input); + if (intent == null) { + Toast.makeText(getActivity(), R.string.msg_no_setup_activity, Toast.LENGTH_SHORT) + .show(); + return; + } + // Even though other app can handle the intent, the setup launched by Live channels + // 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. + 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; + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + 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; + + SetupSourcesAdapter(RecyclerView.Adapter adapter) { + mGuidedActionAdapter = adapter; + } + + @Override + public int getItemViewType(int position) { + if (mShowDivider && position == mKnownInputStartIndex) { + return VIEW_TYPE_DIVIDER; + } + return VIEW_TYPE_INPUT; + } + + @Override + public int getItemCount() { + if (mInputList == null) { + return 0; + } + return mInputList.size() + (mShowDivider ? 1 : 0); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == VIEW_TYPE_INPUT) { + return mGuidedActionAdapter.onCreateViewHolder(parent, viewType); + } + 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; + } + 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 baeb1b29..f8cd8ee7 100644 --- a/src/com/android/tv/onboarding/WelcomeFragment.java +++ b/src/com/android/tv/onboarding/WelcomeFragment.java @@ -16,16 +16,22 @@ package com.android.tv.onboarding; -import android.app.FragmentTransaction; +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.animation.AnimatorSet; import android.os.Bundle; +import android.transition.TransitionValues; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; -import android.widget.Button; +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.animation.SetupAnimationHelper; /** * A fragment for the onboarding screen. @@ -33,35 +39,670 @@ import com.android.tv.common.ui.setup.SetupFragment; public class WelcomeFragment extends SetupFragment { 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 WELCOME_PAGE_TRANSITION_DURATION_MS = 417; + + private static final long BLUE_SCREEN_HOLD_DURATION_MS = 1500; + + private static final int[] TV_FRAMES_1_START = { + R.drawable.tv_1a_01, + R.drawable.tv_1a_02, + R.drawable.tv_1a_03, + R.drawable.tv_1a_04, + R.drawable.tv_1a_05, + R.drawable.tv_1a_06, + R.drawable.tv_1a_07, + R.drawable.tv_1a_08, + R.drawable.tv_1a_09, + R.drawable.tv_1a_10, + R.drawable.tv_1a_11, + R.drawable.tv_1a_12, + R.drawable.tv_1a_13, + R.drawable.tv_1a_14, + R.drawable.tv_1a_15, + R.drawable.tv_1a_16, + R.drawable.tv_1a_17, + R.drawable.tv_1a_18, + R.drawable.tv_1a_19, + R.drawable.tv_1a_20, + 0 + }; + + private static final int[] TV_FRAMES_1_END = { + R.drawable.tv_1b_01, + R.drawable.tv_1b_02, + R.drawable.tv_1b_03, + R.drawable.tv_1b_04, + R.drawable.tv_1b_05, + R.drawable.tv_1b_06, + R.drawable.tv_1b_07, + R.drawable.tv_1b_08, + R.drawable.tv_1b_09, + R.drawable.tv_1b_10, + R.drawable.tv_1b_11, + 0 + }; + + private static final int[] TV_FRAMES_2_BLUE_ARROW = { + R.drawable.arrow_blue_00, + R.drawable.arrow_blue_01, + R.drawable.arrow_blue_02, + R.drawable.arrow_blue_03, + R.drawable.arrow_blue_04, + R.drawable.arrow_blue_05, + R.drawable.arrow_blue_06, + R.drawable.arrow_blue_07, + R.drawable.arrow_blue_08, + R.drawable.arrow_blue_09, + R.drawable.arrow_blue_10, + R.drawable.arrow_blue_11, + R.drawable.arrow_blue_12, + R.drawable.arrow_blue_13, + R.drawable.arrow_blue_14, + R.drawable.arrow_blue_15, + R.drawable.arrow_blue_16, + R.drawable.arrow_blue_17, + R.drawable.arrow_blue_18, + R.drawable.arrow_blue_19, + R.drawable.arrow_blue_20, + R.drawable.arrow_blue_21, + R.drawable.arrow_blue_22, + R.drawable.arrow_blue_23, + R.drawable.arrow_blue_24, + R.drawable.arrow_blue_25, + R.drawable.arrow_blue_26, + R.drawable.arrow_blue_27, + R.drawable.arrow_blue_28, + R.drawable.arrow_blue_29, + R.drawable.arrow_blue_30, + R.drawable.arrow_blue_31, + R.drawable.arrow_blue_32, + R.drawable.arrow_blue_33, + R.drawable.arrow_blue_34, + R.drawable.arrow_blue_35, + R.drawable.arrow_blue_36, + R.drawable.arrow_blue_37, + R.drawable.arrow_blue_38, + R.drawable.arrow_blue_39, + R.drawable.arrow_blue_40, + R.drawable.arrow_blue_41, + R.drawable.arrow_blue_42, + R.drawable.arrow_blue_43, + R.drawable.arrow_blue_44, + R.drawable.arrow_blue_45, + R.drawable.arrow_blue_46, + R.drawable.arrow_blue_47, + R.drawable.arrow_blue_48, + R.drawable.arrow_blue_49, + R.drawable.arrow_blue_50, + R.drawable.arrow_blue_51, + R.drawable.arrow_blue_52, + R.drawable.arrow_blue_53, + R.drawable.arrow_blue_54, + R.drawable.arrow_blue_55, + R.drawable.arrow_blue_56, + R.drawable.arrow_blue_57, + R.drawable.arrow_blue_58, + R.drawable.arrow_blue_59, + R.drawable.arrow_blue_60, + 0 + }; + + private static final int[] TV_FRAMES_2_BLUE_START = { + R.drawable.tv_2a_01, + R.drawable.tv_2a_02, + R.drawable.tv_2a_03, + R.drawable.tv_2a_04, + R.drawable.tv_2a_05, + R.drawable.tv_2a_06, + R.drawable.tv_2a_07, + R.drawable.tv_2a_08, + R.drawable.tv_2a_09, + R.drawable.tv_2a_10, + R.drawable.tv_2a_11, + R.drawable.tv_2a_12, + R.drawable.tv_2a_13, + R.drawable.tv_2a_14, + R.drawable.tv_2a_15, + R.drawable.tv_2a_16, + R.drawable.tv_2a_17, + R.drawable.tv_2a_18, + R.drawable.tv_2a_19, + 0 + }; + + private static final int[] TV_FRAMES_2_BLUE_END = { + R.drawable.tv_2b_01, + R.drawable.tv_2b_02, + R.drawable.tv_2b_03, + R.drawable.tv_2b_04, + R.drawable.tv_2b_05, + R.drawable.tv_2b_06, + R.drawable.tv_2b_07, + R.drawable.tv_2b_08, + R.drawable.tv_2b_09, + R.drawable.tv_2b_10, + R.drawable.tv_2b_11, + R.drawable.tv_2b_12, + R.drawable.tv_2b_13, + R.drawable.tv_2b_14, + R.drawable.tv_2b_15, + R.drawable.tv_2b_16, + R.drawable.tv_2b_17, + R.drawable.tv_2b_18, + R.drawable.tv_2b_19, + 0 + }; + + private static final int[] TV_FRAMES_2_ORANGE_ARROW = { + R.drawable.arrow_orange_180, + R.drawable.arrow_orange_181, + R.drawable.arrow_orange_182, + R.drawable.arrow_orange_183, + R.drawable.arrow_orange_184, + R.drawable.arrow_orange_185, + R.drawable.arrow_orange_186, + R.drawable.arrow_orange_187, + R.drawable.arrow_orange_188, + R.drawable.arrow_orange_189, + R.drawable.arrow_orange_190, + R.drawable.arrow_orange_191, + R.drawable.arrow_orange_192, + R.drawable.arrow_orange_193, + R.drawable.arrow_orange_194, + R.drawable.arrow_orange_195, + R.drawable.arrow_orange_196, + R.drawable.arrow_orange_197, + R.drawable.arrow_orange_198, + R.drawable.arrow_orange_199, + R.drawable.arrow_orange_200, + R.drawable.arrow_orange_201, + R.drawable.arrow_orange_202, + R.drawable.arrow_orange_203, + R.drawable.arrow_orange_204, + R.drawable.arrow_orange_205, + R.drawable.arrow_orange_206, + R.drawable.arrow_orange_207, + R.drawable.arrow_orange_208, + R.drawable.arrow_orange_209, + R.drawable.arrow_orange_210, + R.drawable.arrow_orange_211, + R.drawable.arrow_orange_212, + R.drawable.arrow_orange_213, + R.drawable.arrow_orange_214, + R.drawable.arrow_orange_215, + R.drawable.arrow_orange_216, + R.drawable.arrow_orange_217, + R.drawable.arrow_orange_218, + R.drawable.arrow_orange_219, + R.drawable.arrow_orange_220, + R.drawable.arrow_orange_221, + R.drawable.arrow_orange_222, + R.drawable.arrow_orange_223, + R.drawable.arrow_orange_224, + R.drawable.arrow_orange_225, + R.drawable.arrow_orange_226, + R.drawable.arrow_orange_227, + R.drawable.arrow_orange_228, + R.drawable.arrow_orange_229, + R.drawable.arrow_orange_230, + R.drawable.arrow_orange_231, + R.drawable.arrow_orange_232, + R.drawable.arrow_orange_233, + R.drawable.arrow_orange_234, + R.drawable.arrow_orange_235, + R.drawable.arrow_orange_236, + R.drawable.arrow_orange_237, + R.drawable.arrow_orange_238, + R.drawable.arrow_orange_239, + R.drawable.arrow_orange_240, + 0 + }; + + private static final int[] TV_FRAMES_2_ORANGE_START = { + R.drawable.tv_2c_01, + R.drawable.tv_2c_02, + R.drawable.tv_2c_03, + R.drawable.tv_2c_04, + R.drawable.tv_2c_05, + R.drawable.tv_2c_06, + R.drawable.tv_2c_07, + R.drawable.tv_2c_08, + R.drawable.tv_2c_09, + R.drawable.tv_2c_10, + R.drawable.tv_2c_11, + R.drawable.tv_2c_12, + R.drawable.tv_2c_13, + R.drawable.tv_2c_14, + R.drawable.tv_2c_15, + R.drawable.tv_2c_16, + 0 + }; + + private static final int[] TV_FRAMES_3_START = { + R.drawable.tv_3a_01, + R.drawable.tv_3a_02, + R.drawable.tv_3a_03, + R.drawable.tv_3a_04, + R.drawable.tv_3a_05, + R.drawable.tv_3a_06, + R.drawable.tv_3a_07, + R.drawable.tv_3a_08, + R.drawable.tv_3a_09, + R.drawable.tv_3a_10, + R.drawable.tv_3a_11, + R.drawable.tv_3a_12, + R.drawable.tv_3a_13, + R.drawable.tv_3a_14, + R.drawable.tv_3a_15, + R.drawable.tv_3a_16, + R.drawable.tv_3a_17, + R.drawable.tv_3b_75, + R.drawable.tv_3b_76, + R.drawable.tv_3b_77, + R.drawable.tv_3b_78, + R.drawable.tv_3b_79, + R.drawable.tv_3b_80, + R.drawable.tv_3b_81, + R.drawable.tv_3b_82, + R.drawable.tv_3b_83, + R.drawable.tv_3b_84, + R.drawable.tv_3b_85, + R.drawable.tv_3b_86, + R.drawable.tv_3b_87, + R.drawable.tv_3b_88, + R.drawable.tv_3b_89, + R.drawable.tv_3b_90, + R.drawable.tv_3b_91, + R.drawable.tv_3b_92, + R.drawable.tv_3b_93, + R.drawable.tv_3b_94, + R.drawable.tv_3b_95, + R.drawable.tv_3b_96, + R.drawable.tv_3b_97, + R.drawable.tv_3b_98, + R.drawable.tv_3b_99, + R.drawable.tv_3b_100, + R.drawable.tv_3b_101, + R.drawable.tv_3b_102, + R.drawable.tv_3b_103, + R.drawable.tv_3b_104, + R.drawable.tv_3b_105, + R.drawable.tv_3b_106, + R.drawable.tv_3b_107, + R.drawable.tv_3b_108, + R.drawable.tv_3b_109, + R.drawable.tv_3b_110, + R.drawable.tv_3b_111, + R.drawable.tv_3b_112, + R.drawable.tv_3b_113, + R.drawable.tv_3b_114, + 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 + }; + private int mNumPages; private String[] mPageTitles; private String[] mPageDescriptions; private int mCurrentPageIndex; + private int mPageTransitionDistance; + private ImageView mTvContentView; private PagingIndicator mPageIndicator; - private Button mButton; + private ImageView mArrowView; + private View mLogoView; + + private Animator mAnimator; + + 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; + } + })); + } @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); mPageTitles = getResources().getStringArray(R.array.welcome_page_titles); mPageDescriptions = getResources().getStringArray(R.array.welcome_page_descriptions); mNumPages = mPageTitles.length; mCurrentPageIndex = 0; + mTvContentView = (ImageView) view.findViewById(R.id.tv_content); mPageIndicator = (PagingIndicator) view.findViewById(R.id.page_indicator); mPageIndicator.setPageCount(mNumPages); - mButton = (Button) view.findViewById(R.id.button); - mButton.setOnClickListener(new OnClickListener() { + 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; } @@ -83,19 +724,64 @@ public class WelcomeFragment extends SetupFragment { return fragment; } - private void showPage(int pageIndex) { + private void showPage(final int pageIndex) { SetupFragment fragment = getPage(pageIndex); - FragmentTransaction ft = getFragmentManager().beginTransaction(); - if (pageIndex != 0) { - ft.setCustomAnimations(SetupFragment.ANIM_ENTER, - SetupFragment.ANIM_EXIT); + if (pageIndex == 0) { + fragment.enableFragmentTransition(FRAGMENT_EXIT_TRANSITION); } - ft.replace(R.id.page_container, fragment).commit(); if (pageIndex == mNumPages - 1) { - mButton.setText(R.string.welcome_start_button_text); - } else { - mButton.setText(R.string.welcome_next_button_text); + 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 int[] getParentIdsForDelay() { + return new int[] {R.id.welcome_fragment_root}; + } + + private void startTvFrameAnimation(int newPageIndex) { + if (mAnimator != null) { + mAnimator.cancel(); + } + // TODO: Change the magic numbers to constants once the animation specification is given. + AnimatorSet animatorSet = new AnimatorSet(); + switch (newPageIndex) { + case 1: + mLogoView.setVisibility(View.GONE); + animatorSet.playSequentially( + SetupAnimationHelper.createFrameAnimator(mTvContentView, TV_FRAMES_1_END), + SetupAnimationHelper.createFrameAnimator(mArrowView, + TV_FRAMES_2_BLUE_ARROW), + SetupAnimationHelper.createFrameAnimator(mTvContentView, + TV_FRAMES_2_BLUE_START), + SetupAnimationHelper.createFrameAnimatorWithDelay(mTvContentView, + TV_FRAMES_2_BLUE_END, BLUE_SCREEN_HOLD_DURATION_MS), + SetupAnimationHelper.createFrameAnimator(mArrowView, + TV_FRAMES_2_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)); + break; + case 3: + animatorSet.playSequentially( + SetupAnimationHelper.createFadeOutAnimator(mTvContentView, 333, true), + SetupAnimationHelper.createFrameAnimator(mTvContentView, + TV_FRAMES_4_START)); + break; + } + mAnimator = SetupAnimationHelper.applyAnimationTimeScale(animatorSet); + mAnimator.start(); + } } diff --git a/src/com/android/tv/onboarding/WelcomePageFragment.java b/src/com/android/tv/onboarding/WelcomePageFragment.java index 3c6cd679..28499f1d 100644 --- a/src/com/android/tv/onboarding/WelcomePageFragment.java +++ b/src/com/android/tv/onboarding/WelcomePageFragment.java @@ -32,6 +32,10 @@ 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) { @@ -40,10 +44,15 @@ public class WelcomePageFragment extends SetupFragment { ((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 55d3cf3a..2db877f7 100644 --- a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java +++ b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java @@ -25,6 +25,7 @@ import android.media.AudioManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import com.android.tv.ApplicationSingletons; import com.android.tv.TvApplication; import com.android.tv.analytics.Analytics; import com.android.tv.analytics.Tracker; @@ -62,9 +63,9 @@ public final class AudioCapabilitiesReceiver { public AudioCapabilitiesReceiver(@NonNull Context context, @Nullable OnAc3PassthroughCapabilityChangeListener listener) { mContext = context; - TvApplication tvApplication = (TvApplication) context.getApplicationContext(); - mAnalytics = tvApplication.getAnalytics(); - mTracker = tvApplication.getTracker(); + ApplicationSingletons appSingletons = TvApplication.getSingletons(context); + mAnalytics = appSingletons.getAnalytics(); + mTracker = appSingletons.getTracker(); mListener = listener; } diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java index 61a9baa9..2b997c32 100644 --- a/src/com/android/tv/receiver/BootCompletedReceiver.java +++ b/src/com/android/tv/receiver/BootCompletedReceiver.java @@ -21,20 +21,33 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.util.Log; import com.android.tv.Features; import com.android.tv.TvActivity; +import com.android.tv.dvr.DvrRecordingService; import com.android.tv.recommendation.NotificationService; import com.android.tv.util.OnboardingUtils; import com.android.tv.util.SetupUtils; /** - * Boot completed receiver. It's used to start the {@code NotificationService} for recommendation, - * grant permission to the TIS's and enable {@code TvActivity} if necessary. + * Boot completed receiver for TV app. + * + * <p>It's used to + * <ul> + * <li>start the {@link NotificationService} for recommendation</li> + * <li>grant permission to the TIS's </li> + * <li>enable {@link TvActivity} if necessary</li> + * <li>start the {@link DvrRecordingService} </li> + * </ul> */ public class BootCompletedReceiver extends BroadcastReceiver { + private static final String TAG = "BootCompletedReceiver"; + private static final boolean DEBUG = false; + @Override public void onReceive(Context context, Intent intent) { + if (DEBUG) Log.d(TAG, "boot completed " + intent); // Start {@link NotificationService}. Intent notificationIntent = new Intent(context, NotificationService.class); notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION); @@ -43,8 +56,7 @@ public class BootCompletedReceiver extends BroadcastReceiver { // Grant permission to already set up packages after the system has finished booting. SetupUtils.grantEpgPermissionToSetUpPackages(context); - // On-boarding experience. - if (Features.ONBOARDING_EXPERIENCE.isEnabled(context)) { + if (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. @@ -58,5 +70,10 @@ public class BootCompletedReceiver extends BroadcastReceiver { OnboardingUtils.setFirstBootCompleted(context); } } + + // DVR + if (Features.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 a6ab858d..cb35c87a 100644 --- a/src/com/android/tv/receiver/PackageIntentsReceiver.java +++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java @@ -21,82 +21,55 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; -import android.media.tv.TvInputInfo; -import android.media.tv.TvInputManager; -import android.os.Handler; -import com.android.tv.Features; import com.android.tv.TvActivity; -import com.android.tv.util.SetupUtils; - -import java.util.List; +import com.android.tv.TvApplication; +import com.android.usbtuner.TunerSetupActivity; +import com.android.usbtuner.UsbTunerPreferences; +import com.android.usbtuner.tvinput.UsbTunerTvInputService; /** * A class for handling the broadcast intents from PackageManager. */ public class PackageIntentsReceiver extends BroadcastReceiver { - // Delay before checking TvInputManager's input list. - // Sometimes TvInputManager's input list isn't updated yet when this receiver is called. - // So we should check the list after some delay. - private static final long TV_INPUT_UPDATE_DELAY_MS = 500; - - private TvInputManager mTvInputManager; - private final Handler mHandler = new Handler(); - private Runnable mOnPackageUpdatedRunnable; private PackageManager mPackageManager; private ComponentName mTvActivityComponentName; + private ComponentName mUsbTunerComponentName; private void init(Context context) { - mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); - - final Context applicationContext = context.getApplicationContext(); - mOnPackageUpdatedRunnable = new Runnable() { - @Override - public void run() { - if (!Features.ONBOARDING_EXPERIENCE.isEnabled(applicationContext)) { - List<TvInputInfo> inputs = mTvInputManager.getTvInputList(); - // Enable the MainActivity only if there is at least one tuner type input. - boolean enable = false; - for (TvInputInfo input : inputs) { - if (input.getType() == TvInputInfo.TYPE_TUNER) { - enable = true; - break; - } - } - enableTvActivityWithinPackageManager(applicationContext, enable); - } - - SetupUtils.getInstance(applicationContext).onInputListUpdated(mTvInputManager); - } - }; - - mPackageManager = applicationContext.getPackageManager(); - mTvActivityComponentName = new ComponentName(applicationContext, TvActivity.class); + mPackageManager = context.getPackageManager(); + mTvActivityComponentName = new ComponentName(context, TvActivity.class); + mUsbTunerComponentName = new ComponentName(context, UsbTunerTvInputService.class); } @Override public void onReceive(Context context, Intent intent) { - if (mTvInputManager == null) { + if (mPackageManager == null) { init(context); } - - mHandler.removeCallbacks(mOnPackageUpdatedRunnable); - mHandler.postDelayed(mOnPackageUpdatedRunnable, TV_INPUT_UPDATE_DELAY_MS); - + ((TvApplication) context.getApplicationContext()).handleInputCountChanged(); + // Check the component status of UsbTunerTvInputService and TvActivity here to make sure + // start the setup activity of USB tuner TV input service only when those components are + // enabled. + if (UsbTunerPreferences.shouldShowSetupActivity(context) + && Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction()) + && mPackageManager.getComponentEnabledSetting(mTvActivityComponentName) + == PackageManager.COMPONENT_ENABLED_STATE_ENABLED + && mPackageManager.getComponentEnabledSetting(mUsbTunerComponentName) + == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + startUsbTunerSetupActivity(context); + UsbTunerPreferences.setShouldShowSetupActivity(context, false); + } } - /** - * Enables/Disables {@link TvActivity}. + * Launches the setup activity of USB tuner TV input service. + * + * @param context {@link Context} instance */ - private void enableTvActivityWithinPackageManager(Context context, boolean enable) { - PackageManager pm = context.getPackageManager(); - ComponentName name = new ComponentName(context, TvActivity.class); - - int newState = enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : - PackageManager.COMPONENT_ENABLED_STATE_DISABLED; - if (pm.getComponentEnabledSetting(name) != newState) { - pm.setComponentEnabledSetting(name, newState, 0); - } + private static void startUsbTunerSetupActivity(Context context) { + Intent intent = TunerSetupActivity.createSetupActivity(context); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); } } diff --git a/src/com/android/tv/recommendation/ChannelRecord.java b/src/com/android/tv/recommendation/ChannelRecord.java index 10aaeef7..26f0fbf0 100644 --- a/src/com/android/tv/recommendation/ChannelRecord.java +++ b/src/com/android/tv/recommendation/ChannelRecord.java @@ -19,8 +19,10 @@ package com.android.tv.recommendation; import android.content.Context; import android.support.annotation.VisibleForTesting; +import com.android.tv.TvApplication; import com.android.tv.data.Channel; import com.android.tv.data.Program; +import com.android.tv.data.ProgramDataManager; import com.android.tv.util.Utils; import java.util.ArrayDeque; @@ -68,7 +70,9 @@ public class ChannelRecord { public Program getCurrentProgram() { long time = System.currentTimeMillis(); if (mCurrentProgram == null || mCurrentProgram.getEndTimeUtcMillis() < time) { - mCurrentProgram = Utils.getCurrentProgram(mContext, mChannel.getId()); + ProgramDataManager manager = + TvApplication.getSingletons(mContext).getProgramDataManager(); + mCurrentProgram = manager.getCurrentProgram(mChannel.getId()); } return mCurrentProgram; } diff --git a/src/com/android/tv/recommendation/NotificationService.java b/src/com/android/tv/recommendation/NotificationService.java index c00a508e..3ab67c8b 100644 --- a/src/com/android/tv/recommendation/NotificationService.java +++ b/src/com/android/tv/recommendation/NotificationService.java @@ -40,6 +40,7 @@ import android.util.SparseLongArray; import android.view.View; import com.android.tv.R; +import com.android.tv.ApplicationSingletons; import com.android.tv.TvApplication; import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; @@ -141,16 +142,16 @@ public class NotificationService extends Service implements Recommender.Listener getResources().getDimensionPixelOffset(R.dimen.notif_ch_logo_padding_bottom); mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - TvApplication application = ((TvApplication) getApplicationContext()); - mTvInputManagerHelper = application.getTvInputManagerHelper(); + ApplicationSingletons appSingletons = TvApplication.getSingletons(this); + mTvInputManagerHelper = appSingletons.getTvInputManagerHelper(); mHandlerThread = new HandlerThread("tv notification"); mHandlerThread.start(); mHandler = new NotificationHandler(mHandlerThread.getLooper(), this); mHandler.sendEmptyMessage(MSG_INITIALIZE_RECOMMENDER); // Just called for early initialization. - application.getChannelDataManager(); - application.getProgramDataManager(); + appSingletons.getChannelDataManager(); + appSingletons.getProgramDataManager(); } private void handleInitializeRecommender() { diff --git a/src/com/android/tv/recommendation/RecommendationDataManager.java b/src/com/android/tv/recommendation/RecommendationDataManager.java index 693380df..66dd9fe4 100644 --- a/src/com/android/tv/recommendation/RecommendationDataManager.java +++ b/src/com/android/tv/recommendation/RecommendationDataManager.java @@ -30,6 +30,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; @@ -531,23 +532,31 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener /** * A listener interface to receive notification about the recommendation data. + * + * @MainThread */ public interface Listener { /** * Called when loading channel record map from database is finished. * It will be called after RecommendationDataManager.start() is finished. + * + * <p>Note that this method is called on the main thread. */ void onChannelRecordLoaded(); /** * Called when a new watch log is added into the corresponding channelRecord. * + * <p>Note that this method is called on the main thread. + * * @param channelRecord The channel record corresponds to the new watch log. */ void onNewWatchLog(ChannelRecord channelRecord); /** * Called when the channel record map changes. + * + * <p>Note that this method is called on the main thread. */ void onChannelRecordChanged(); } diff --git a/src/com/android/tv/search/DataManagerSearch.java b/src/com/android/tv/search/DataManagerSearch.java index 4c85af67..88c69c53 100644 --- a/src/com/android/tv/search/DataManagerSearch.java +++ b/src/com/android/tv/search/DataManagerSearch.java @@ -26,6 +26,7 @@ import android.support.annotation.UiThread; import android.text.TextUtils; import android.util.Log; +import com.android.tv.ApplicationSingletons; import com.android.tv.TvApplication; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; @@ -60,9 +61,9 @@ public class DataManagerSearch implements SearchInterface { DataManagerSearch(Context context) { mContext = context; mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); - TvApplication application = (TvApplication) context.getApplicationContext(); - mChannelDataManager = application.getChannelDataManager(); - mProgramDataManager = application.getProgramDataManager(); + ApplicationSingletons appSingletons = TvApplication.getSingletons(context); + mChannelDataManager = appSingletons.getChannelDataManager(); + mProgramDataManager = appSingletons.getProgramDataManager(); } @Override diff --git a/src/com/android/tv/search/SearchInterface.java b/src/com/android/tv/search/SearchInterface.java index 7394150e..caa45812 100644 --- a/src/com/android/tv/search/SearchInterface.java +++ b/src/com/android/tv/search/SearchInterface.java @@ -37,5 +37,5 @@ public interface SearchInterface { * @param action One of {@link #ACTION_TYPE_SWITCH_CHANNEL}, {@link #ACTION_TYPE_SWITCH_INPUT}, * or {@link #ACTION_TYPE_AMBIGUOUS}, */ - public List<SearchResult> search(String query, int limit, int action); + List<SearchResult> search(String query, int limit, int action); } diff --git a/src/com/android/tv/ui/AppLayerTvView.java b/src/com/android/tv/ui/AppLayerTvView.java index c7b94a15..23ac5392 100644 --- a/src/com/android/tv/ui/AppLayerTvView.java +++ b/src/com/android/tv/ui/AppLayerTvView.java @@ -16,8 +16,9 @@ package com.android.tv.ui; +import com.android.tv.common.dvr.DvrTvView; + import android.content.Context; -import android.media.tv.TvView; import android.util.AttributeSet; /** @@ -29,7 +30,7 @@ import android.util.AttributeSet; * TODO: remove this class once the TvView.setMain() is revisited. * </p> */ -public class AppLayerTvView extends TvView { +public class AppLayerTvView extends DvrTvView { public AppLayerTvView(Context context) { super(context); } diff --git a/src/com/android/tv/ui/KeypadChannelSwitchView.java b/src/com/android/tv/ui/KeypadChannelSwitchView.java index 722a3759..140ba533 100644 --- a/src/com/android/tv/ui/KeypadChannelSwitchView.java +++ b/src/com/android/tv/ui/KeypadChannelSwitchView.java @@ -111,7 +111,7 @@ public class KeypadChannelSwitchView extends LinearLayout implements super(context, attrs, defStyleAttr); mMainActivity = (MainActivity) context; - mTracker = ((TvApplication) mMainActivity.getApplication()).getTracker(); + mTracker = TvApplication.getSingletons(context).getTracker(); Resources resources = getResources(); mLayoutInflater = LayoutInflater.from(context); mShowDurationMillis = resources.getInteger(R.integer.keypad_channel_switch_show_duration); diff --git a/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java b/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java index 2a59e6f6..afea9ba5 100644 --- a/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java +++ b/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java @@ -28,7 +28,7 @@ import com.android.tv.common.WeakHandler; * Listener to make focus change faster over time. */ public class OnRepeatedKeyInterceptListener implements VerticalGridView.OnKeyInterceptListener { - private static final String TAG = "OnRepeatedKeyInterceptListener"; + private static final String TAG = "OnRepeatedKeyListener"; private static final boolean DEBUG = false; private static final int[] THRESHOLD_FAST_FOCUS_CHANGE_TIME_MS = { 2000, 5000 }; diff --git a/src/com/android/tv/ui/SelectInputView.java b/src/com/android/tv/ui/SelectInputView.java index b5b6ef34..032782bd 100644 --- a/src/com/android/tv/ui/SelectInputView.java +++ b/src/com/android/tv/ui/SelectInputView.java @@ -35,6 +35,7 @@ import android.view.ViewGroup; import android.widget.TextView; import com.android.tv.R; +import com.android.tv.ApplicationSingletons; import com.android.tv.TvApplication; import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.Tracker; @@ -56,7 +57,6 @@ public class SelectInputView extends VerticalGridView implements public static final String SCREEN_NAME = "Input selection"; private static final int TUNER_INPUT_POSITION = 0; - private final TvApplication mApplication; private final TvInputManagerHelper mTvInputManagerHelper; private final List<TvInputInfo> mInputList = new ArrayList<>(); private final InputsComparator mComparator = new InputsComparator(); @@ -147,9 +147,9 @@ public class SelectInputView extends VerticalGridView implements super(context, attrs, defStyleAttr); setAdapter(new InputListAdapter()); - mApplication = (TvApplication) context.getApplicationContext(); - mTracker = ((TvApplication) context.getApplicationContext()).getTracker(); - mTvInputManagerHelper = mApplication.getTvInputManagerHelper(); + ApplicationSingletons appSingletons = TvApplication.getSingletons(context); + mTracker = appSingletons.getTracker(); + mTvInputManagerHelper = appSingletons.getTvInputManagerHelper(); Resources resources = context.getResources(); mInputItemHeight = resources.getDimensionPixelSize(R.dimen.input_banner_item_height); @@ -453,7 +453,7 @@ public class SelectInputView extends VerticalGridView implements /** * A callback interface for the input selection. */ - public static interface OnInputSelectedCallback { + public interface OnInputSelectedCallback { /** * Called when the tuner input is selected. */ diff --git a/src/com/android/tv/ui/SetupView.java b/src/com/android/tv/ui/SetupView.java index cb25f6f9..95a9f28e 100644 --- a/src/com/android/tv/ui/SetupView.java +++ b/src/com/android/tv/ui/SetupView.java @@ -26,7 +26,6 @@ import android.app.Dialog; import android.content.Context; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager.TvInputCallback; -import android.support.annotation.VisibleForTesting; import android.support.v17.leanback.widget.VerticalGridView; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; @@ -41,12 +40,12 @@ 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.Comparator; import java.util.List; public class SetupView extends FullscreenDialogView { @@ -206,7 +205,7 @@ public class SetupView extends FullscreenDialogView { mInputList = new ArrayList<>(); mKnownInputStartIndex = 0; mInputList = mInputManager.getTvInputInfos(true, true); - Collections.sort(mInputList, new TvInputInfoComparator(mSetupUtils, mInputManager)); + Collections.sort(mInputList, new TvInputNewComparator(mSetupUtils, mInputManager)); for (TvInputInfo input : mInputList) { if (mSetupUtils.isNewInput(input.getId())) { mSetupUtils.markAsKnownInput(input.getId()); @@ -417,25 +416,4 @@ public class SetupView extends FullscreenDialogView { mDescription = (TextView) itemView.findViewById(R.id.description); } } - - @VisibleForTesting - static class TvInputInfoComparator implements Comparator<TvInputInfo> { - private final SetupUtils mSetupUtils; - private final TvInputManagerHelper mInputManager; - - public TvInputInfoComparator(SetupUtils setupUtils, TvInputManagerHelper inputManager) { - mSetupUtils = setupUtils; - mInputManager = inputManager; - } - - @Override - public int compare(TvInputInfo lhs, TvInputInfo rhs) { - boolean lhsIsNewInput = mSetupUtils.isNewInput(lhs.getId()); - boolean rhsIsNewInput = mSetupUtils.isNewInput(rhs.getId()); - if (lhsIsNewInput != rhsIsNewInput) { - return lhsIsNewInput ? -1 : 1; - } - return mInputManager.getDefaultTvInputInfoComparator().compare(lhs, rhs); - } - } } diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java index f526c33c..fe185b2e 100644 --- a/src/com/android/tv/ui/TunableTvView.java +++ b/src/com/android/tv/ui/TunableTvView.java @@ -48,6 +48,7 @@ import android.widget.ImageView; import android.widget.TextView; import com.android.tv.R; +import com.android.tv.ApplicationSingletons; import com.android.tv.TvApplication; import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.Tracker; @@ -331,9 +332,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo { super(context, attrs, defStyleAttr, defStyleRes); inflate(getContext(), R.layout.tunable_tv_view, this); - TvApplication tvApplication = (TvApplication) context.getApplicationContext(); + ApplicationSingletons appSingletons = TvApplication.getSingletons(context); mCanModifyParentalControls = PermissionUtils.hasModifyParentalControls(context); - mTracker = tvApplication.getTracker(); + mTracker = appSingletons.getTracker(); mBlockScreenType = BLOCK_SCREEN_TYPE_NORMAL; mBlockScreenView = findViewById(R.id.block_screen); mBlockScreenDescriptionView = findViewById(R.id.block_screen_description); diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java index b3c009d8..2bd4fcc5 100644 --- a/src/com/android/tv/ui/TvOverlayManager.java +++ b/src/com/android/tv/ui/TvOverlayManager.java @@ -24,6 +24,7 @@ import android.util.Log; import android.view.KeyEvent; import android.view.ViewGroup; +import com.android.tv.ApplicationSingletons; import com.android.tv.ChannelTuner; import com.android.tv.MainActivity; import com.android.tv.MainActivity.KeyHandlerResultType; @@ -133,7 +134,8 @@ public class TvOverlayManager { mKeypadChannelSwitchView = keypadChannelSwitchView; mSelectInputView = selectInputView; mSearchFragment = searchFragment; - mTracker = ((TvApplication) mainActivity.getApplication()).getTracker(); + ApplicationSingletons singletons = TvApplication.getSingletons(mainActivity); + mTracker = singletons.getTracker(); mTransitionManager = new TvTransitionManager(mainActivity, sceneContainer, channelBannerView, inputBannerView, mKeypadChannelSwitchView, selectInputView); mTransitionManager.setListener(new TvTransitionManager.Listener() { @@ -179,22 +181,22 @@ public class TvOverlayManager { } }); // Program Guide + Runnable preShowRunnable = new Runnable() { + @Override + public void run() { + onOverlayOpened(OVERLAY_TYPE_GUIDE); + } + }; + Runnable postHideRunnable = new Runnable() { + @Override + public void run() { + onOverlayClosed(OVERLAY_TYPE_GUIDE); + } + }; mProgramGuide = new ProgramGuide(mainActivity, channelTuner, - mainActivity.getTvInputManagerHelper(), mainActivity.getChannelDataManager(), - mainActivity.getProgramDataManager(), - ((TvApplication) mainActivity.getApplication()).getTracker(), - new Runnable() { - @Override - public void run() { - onOverlayOpened(OVERLAY_TYPE_GUIDE); - } - }, - new Runnable() { - @Override - public void run() { - onOverlayClosed(OVERLAY_TYPE_GUIDE); - } - }); + singletons.getTvInputManagerHelper(), singletons.getChannelDataManager(), + singletons.getProgramDataManager(), singletons.getTracker(), preShowRunnable, + postHideRunnable); } /** diff --git a/src/com/android/tv/ui/sidepanel/AboutFragment.java b/src/com/android/tv/ui/sidepanel/AboutFragment.java index e880fe37..ee83e21e 100644 --- a/src/com/android/tv/ui/sidepanel/AboutFragment.java +++ b/src/com/android/tv/ui/sidepanel/AboutFragment.java @@ -18,8 +18,10 @@ 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.CompoundButton; import android.widget.TextView; import com.android.tv.Features; @@ -98,8 +100,7 @@ public class AboutFragment extends SideFragment { super(context.getResources().getString(R.string.about_menu_improve), context.getResources().getString(R.string.about_menu_improve), context.getResources().getString(R.string.about_menu_improve_summary)); - mPreferenceHelper = ((TvApplication) context.getApplicationContext()) - .getOptPreferenceHelper(); + mPreferenceHelper = TvApplication.getSingletons(context).getOptPreferenceHelper(); } @Override @@ -140,9 +141,9 @@ public class AboutFragment extends SideFragment { if (mMainActivity != null && mBoundView != null && mBoundView.hasFocus()) { // Quick fix for accessibility // TODO: Need to change the resource in the future. - mMainActivity.sendAccessiblityText(checked ? - mMainActivity.getString(R.string.options_item_pip_on) - : mMainActivity.getString(R.string.options_item_pip_off)); + mMainActivity.sendAccessibilityText( + checked ? mMainActivity.getString(R.string.options_item_pip_on) + : mMainActivity.getString(R.string.options_item_pip_off)); } } } @@ -168,6 +169,19 @@ public class AboutFragment extends SideFragment { 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/DeveloperFragment.java b/src/com/android/tv/ui/sidepanel/DeveloperFragment.java new file mode 100644 index 00000000..13f6c866 --- /dev/null +++ b/src/com/android/tv/ui/sidepanel/DeveloperFragment.java @@ -0,0 +1,114 @@ +/* + * 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.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 developer options like enabling USB TV tuner. + */ +public class DeveloperFragment extends SideFragment { + private static final String TRACKER_LABEL = "developer options"; + + /** + * Sets USB TV tuner enabled. + */ + private static final class UsbTvTunerItem extends SwitchItem { + Context mContext; + + public UsbTvTunerItem(Context context) { + super(context.getResources().getString(R.string.developer_menu_enable_usb_tv_tuner), + context.getResources().getString(R.string.developer_menu_enable_usb_tv_tuner), + context.getResources().getString( + R.string.developer_menu_enable_usb_tv_tuner_description)); + mContext = context; + } + + @Override + protected void onBind(View view) { + super.onBind(view); + setChecked(Features.USB_TUNER.isEnabled(view.getContext())); + } + + @Override + public void setChecked(boolean checked) { + super.setChecked(checked); + Features.USB_TUNER.setEnabled(mContext, checked); + } + } + + /** + * 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); + } + + @Override + public String getTrackerLabel() { + return TRACKER_LABEL; + } + + @Override + protected List<Item> getItemList() { + List<Item> items = new ArrayList<>(); + Activity activity = getActivity(); + items.add(new UsbTvTunerItem(activity)); + items.add(new Ac3CapabilityItem()); + return items; + } +} diff --git a/src/com/android/tv/ui/sidepanel/SideFragment.java b/src/com/android/tv/ui/sidepanel/SideFragment.java index e7072572..8c37f40f 100644 --- a/src/com/android/tv/ui/sidepanel/SideFragment.java +++ b/src/com/android/tv/ui/sidepanel/SideFragment.java @@ -84,7 +84,7 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel { super.onAttach(activity); mChannelDataManager = getMainActivity().getChannelDataManager(); mProgramDataManager = getMainActivity().getProgramDataManager(); - mTracker = ((TvApplication) getMainActivity().getApplicationContext()).getTracker(); + mTracker = TvApplication.getSingletons(activity).getTracker(); } @Override diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java index fbc93f3a..9f440533 100644 --- a/src/com/android/tv/util/AsyncDbTask.java +++ b/src/com/android/tv/util/AsyncDbTask.java @@ -22,7 +22,6 @@ import android.media.tv.TvContract; import android.net.Uri; import android.os.AsyncTask; import android.support.annotation.MainThread; -import android.support.annotation.NonNull; import android.support.annotation.WorkerThread; import android.util.Log; import android.util.Range; @@ -35,8 +34,6 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; /** * {@link AsyncTask} that defaults to executing on its own single threaded Executor Service. @@ -49,28 +46,6 @@ public abstract class AsyncDbTask<Params, Progress, Result> private static final String TAG = "AsyncDbTask"; private static final boolean DEBUG = false; - private static class NamedThreadFactory implements ThreadFactory { - private final AtomicInteger mCount = new AtomicInteger(0); - private final ThreadFactory mDefaultThreadFactory; - private final String mPrefix; - - public NamedThreadFactory(final String baseName) { - mDefaultThreadFactory = Executors.defaultThreadFactory(); - mPrefix = baseName + "-"; - } - - @Override - public Thread newThread(@NonNull final Runnable runnable) { - final Thread thread = mDefaultThreadFactory.newThread(runnable); - thread.setName(mPrefix + mCount.getAndIncrement()); - return thread; - } - - public boolean namedWithPrefix(Thread thread) { - return thread.getName().startsWith(mPrefix); - } - } - public static final NamedThreadFactory THREAD_FACTORY = new NamedThreadFactory( AsyncDbTask.class.getSimpleName()); private static final ExecutorService DB_EXECUTOR = Executors diff --git a/src/com/android/tv/util/Clock.java b/src/com/android/tv/util/Clock.java index f6c3782e..c5e96431 100644 --- a/src/com/android/tv/util/Clock.java +++ b/src/com/android/tv/util/Clock.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2014 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. @@ -13,22 +13,53 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.android.tv.util; +import android.os.SystemClock; + /** - * Interface to provide time APIs for test. + * An interface through which system clocks can be read. The {@link #SYSTEM} implementation + * must be used for all non-test cases. */ public interface Clock { /** * Returns the current time in milliseconds since January 1, 1970 00:00:00.0 UTC. + * See {@link System#currentTimeMillis()}. */ long currentTimeMillis(); + /** + * Returns milliseconds since boot, including time spent in sleep. + * + * @see SystemClock#elapsedRealtime() + */ + long elapsedRealtime(); + + /** + * Waits a given number of milliseconds (of uptimeMillis) before returning. + * + * @param ms to sleep before returning, in milliseconds of uptime. + * @see SystemClock#sleep(long) + */ + void sleep(long ms); + + /** + * The default implementation of Clock. + */ Clock SYSTEM = new Clock() { @Override public long currentTimeMillis() { return System.currentTimeMillis(); } + + @Override + public long elapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + + @Override + public void sleep(long ms) { + SystemClock.sleep(ms); + } }; -}
\ No newline at end of file +} diff --git a/src/com/android/tv/util/CollectionUtils.java b/src/com/android/tv/util/CollectionUtils.java index e07bac90..d1c50392 100644 --- a/src/com/android/tv/util/CollectionUtils.java +++ b/src/com/android/tv/util/CollectionUtils.java @@ -33,9 +33,9 @@ public class CollectionUtils { */ public static <T> Set<T> createSmallSet() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return new ArraySet<T>(); + return new ArraySet<>(); } else { - return new HashSet<T>(); + return new HashSet<>(); } } } diff --git a/src/com/android/tv/util/EngOnlyFeature.java b/src/com/android/tv/util/EngOnlyFeature.java index 904e2369..f4cbe9cf 100644 --- a/src/com/android/tv/util/EngOnlyFeature.java +++ b/src/com/android/tv/util/EngOnlyFeature.java @@ -25,8 +25,17 @@ 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/MainThreadExecutor.java b/src/com/android/tv/util/MainThreadExecutor.java index 817286f7..ce8f8ff3 100644 --- a/src/com/android/tv/util/MainThreadExecutor.java +++ b/src/com/android/tv/util/MainThreadExecutor.java @@ -32,7 +32,7 @@ public class MainThreadExecutor extends AbstractExecutorService { private final static MainThreadExecutor INSTANCE = new MainThreadExecutor(); - public final static MainThreadExecutor getInstance() { + public static MainThreadExecutor getInstance() { return INSTANCE; } diff --git a/src/com/android/tv/util/NamedThreadFactory.java b/src/com/android/tv/util/NamedThreadFactory.java new file mode 100644 index 00000000..fcdde952 --- /dev/null +++ b/src/com/android/tv/util/NamedThreadFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.util; + +import android.support.annotation.NonNull; + +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A thread factory that creates threads with a suffix. + */ +public class NamedThreadFactory implements ThreadFactory { + private final AtomicInteger mCount = new AtomicInteger(0); + private final ThreadFactory mDefaultThreadFactory; + private final String mPrefix; + + public NamedThreadFactory(final String baseName) { + mDefaultThreadFactory = Executors.defaultThreadFactory(); + mPrefix = baseName + "-"; + } + + @Override + public Thread newThread(@NonNull final Runnable runnable) { + final Thread thread = mDefaultThreadFactory.newThread(runnable); + thread.setName(mPrefix + mCount.getAndIncrement()); + return thread; + } + + public boolean namedWithPrefix(Thread thread) { + return thread.getName().startsWith(mPrefix); + } +} diff --git a/src/com/android/tv/util/OnboardingUtils.java b/src/com/android/tv/util/OnboardingUtils.java index c693185e..0570d590 100644 --- a/src/com/android/tv/util/OnboardingUtils.java +++ b/src/com/android/tv/util/OnboardingUtils.java @@ -32,8 +32,6 @@ 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_ARE_CHANNELS_AVAILABLE = - "pref_onbaording_are_channels_available"; /** * Checks if this is the first boot after the onboarding experience has been applied. @@ -84,8 +82,7 @@ public final class OnboardingUtils { */ @UiThread public static boolean areChannelsAvailable(Context context) { - ChannelDataManager manager = ((TvApplication) context.getApplicationContext()) - .getChannelDataManager(); + ChannelDataManager manager = TvApplication.getSingletons(context).getChannelDataManager(); if (manager.isDbLoadFinished()) { return manager.getChannelCount() != 0; } diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java index 46299d3b..0fbbce1e 100644 --- a/src/com/android/tv/util/SetupUtils.java +++ b/src/com/android/tv/util/SetupUtils.java @@ -27,6 +27,7 @@ import android.os.Build; import android.preference.PreferenceManager; import android.util.Log; +import com.android.tv.ApplicationSingletons; import com.android.tv.TvApplication; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; @@ -54,6 +55,7 @@ public class SetupUtils { private final Set<String> mKnownInputs; private final Set<String> mSetUpInputs; private boolean mIsFirstTune; + private final String mUsbTunerInputId; private SetupUtils(TvApplication tvApplication) { mTvApplication = tvApplication; @@ -63,6 +65,8 @@ public class SetupUtils { mKnownInputs = new HashSet<>(mSharedPreferences.getStringSet(PREF_KEY_KNOWN_INPUTS, new HashSet<String>())); mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true); + mUsbTunerInputId = TvContract.buildInputId(new ComponentName(tvApplication, + com.android.usbtuner.tvinput.UsbTunerTvInputService.class)); } /** @@ -107,8 +111,8 @@ public class SetupUtils { private static void updateChannelBrowsable(Context context, final String inputId, final Runnable postRunnable) { - TvApplication tvApplication = (TvApplication) context.getApplicationContext(); - final ChannelDataManager manager = tvApplication.getChannelDataManager(); + ApplicationSingletons appSingletons = TvApplication.getSingletons(context); + final ChannelDataManager manager = appSingletons.getChannelDataManager(); manager.updateChannels(new Runnable() { @Override public void run() { @@ -189,7 +193,16 @@ public class SetupUtils { PREF_KEY_SET_UP_INPUTS, new HashSet<String>())); Set<String> setUpPackages = new HashSet<>(); for (String input : setUpInputs) { - setUpPackages.add(ComponentName.unflattenFromString(input).getPackageName()); + ComponentName componentName = null; + try { + componentName = ComponentName.unflattenFromString(input); + } catch (Exception e) { + Log.w(TAG, "Failed to unflatten string to component name (" + input + ")", e); + } + if (componentName == null) { + continue; + } + setUpPackages.add(componentName.getPackageName()); } for (String packageName : setUpPackages) { grantEpgPermission(context, packageName); @@ -243,6 +256,10 @@ public class SetupUtils { for (TvInputInfo input : manager.getTvInputList()) { removedInputList.remove(input.getId()); } + // A USB tuner device can be temporarily unplugged. We do not remove the USB tuner input + // from the known inputs so that the input won't appear as a new input whenever the user + // plugs in the USB tuner device again. + removedInputList.remove(mUsbTunerInputId); if (!removedInputList.isEmpty()) { for (String input : removedInputList) { @@ -260,6 +277,7 @@ public class SetupUtils { * for {@code inputId}. */ public void onSetupDone(String inputId) { + SoftPreconditions.checkState(inputId != null); if (DEBUG) Log.d(TAG, "onSetupDone: input=" + inputId); if (!mKnownInputs.contains(inputId)) { Log.i(TAG, "An unknown input's setup has been done. inputId=" + inputId); diff --git a/src/com/android/tv/util/SoftPreconditions.java b/src/com/android/tv/util/SoftPreconditions.java index 5c2a8170..b00027a8 100644 --- a/src/com/android/tv/util/SoftPreconditions.java +++ b/src/com/android/tv/util/SoftPreconditions.java @@ -16,10 +16,12 @@ package com.android.tv.util; +import android.content.Context; import android.text.TextUtils; import android.util.Log; import com.android.tv.BuildConfig; +import com.android.tv.common.feature.Feature; /** * Simple static methods to be called at the start of your own methods to verify @@ -116,6 +118,19 @@ public final class SoftPreconditions { } /** + * Throws or logs if the Feature is not enabled + * + * @param context an android context + * @param feature the required feature + * @param tag used to identify the source of a log message. It usually + * identifies the class or activity where the log call occurs + * @throws IllegalStateException if {@code feature} is not enabled + */ + public static void checkFeatureEnabled(Context context, Feature feature, String tag) { + checkState(feature.isEnabled(context), tag, feature.toString()); + } + + /** * Throws a {@link RuntimeException} if {@link BuildConfig#ENG} is true, else log a warning. * * @param tag Used to identify the source of a log message. It usually diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java index d11ecd82..250ca430 100644 --- a/src/com/android/tv/util/TvInputManagerHelper.java +++ b/src/com/android/tv/util/TvInputManagerHelper.java @@ -40,6 +40,7 @@ import java.util.Set; public class TvInputManagerHelper { private static final String TAG = "TvInputManagerHelper"; + private static final boolean DEBUG = false; // Hardcoded list for known bundled inputs not written by OEM/SOCs. // Bundled (system) inputs not in the list will get the high priority @@ -59,6 +60,7 @@ public class TvInputManagerHelper { private final TvInputCallback mInternalCallback = new TvInputCallback() { @Override public void onInputStateChanged(String inputId, int state) { + if (DEBUG) Log.d(TAG, "onInputStateChanged " + inputId + " state=" + state); mInputStateMap.put(inputId, state); for (TvInputCallback callback : mCallbacks) { callback.onInputStateChanged(inputId, state); @@ -67,6 +69,7 @@ public class TvInputManagerHelper { @Override public void onInputAdded(String inputId) { + if (DEBUG) Log.d(TAG, "onInputAdded " + inputId); TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); if (info != null) { mInputMap.put(inputId, info); @@ -81,6 +84,7 @@ public class TvInputManagerHelper { @Override public void onInputRemoved(String inputId) { + if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId); mInputMap.remove(inputId); mInputStateMap.remove(inputId); mInputIdToPartnerInputMap.remove(inputId); @@ -92,6 +96,7 @@ public class TvInputManagerHelper { @Override public void onInputUpdated(String inputId) { + if (DEBUG) Log.d(TAG, "onInputUpdated " + inputId); TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); mInputMap.put(inputId, info); for (TvInputCallback callback : mCallbacks) { @@ -108,7 +113,7 @@ public class TvInputManagerHelper { private final Comparator<TvInputInfo> mTvInputInfoComparator; public TvInputManagerHelper(Context context) { - mContext = context; + mContext = context.getApplicationContext(); mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); mContentRatingsManager = new ContentRatingsManager(context); mParentalControlSettings = new ParentalControlSettings(context); @@ -119,11 +124,14 @@ public class TvInputManagerHelper { if (mStarted) { return; } + if (DEBUG) Log.d(TAG, "start"); mStarted = true; mTvInputManager.registerCallback(mInternalCallback, mHandler); mInputMap.clear(); mInputStateMap.clear(); + mInputIdToPartnerInputMap.clear(); for (TvInputInfo input : mTvInputManager.getTvInputList()) { + if (DEBUG) Log.d(TAG, "Input detected " + input); String inputId = input.getId(); mInputMap.put(inputId, input); int state = mTvInputManager.getInputState(inputId); @@ -143,6 +151,7 @@ public class TvInputManagerHelper { mStarted = false; mInputStateMap.clear(); mInputMap.clear(); + mInputIdToPartnerInputMap.clear(); } public List<TvInputInfo> getTvInputInfos(boolean availableOnly, boolean tunerOnly) { diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java index 81b469fe..5c6d5345 100644 --- a/src/com/android/tv/util/Utils.java +++ b/src/com/android/tv/util/Utils.java @@ -49,6 +49,8 @@ import com.android.tv.TvApplication; 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.Calendar; import java.util.Collection; @@ -67,6 +69,8 @@ public class Utils { private static final String TAG = "Utils"; private static final boolean DEBUG = false; + private static final SimpleDateFormat ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); + public static final String EXTRA_KEY_KEYCODE = "keycode"; public static final String EXTRA_KEY_ACTION = "action"; public static final String EXTRA_ACTION_SHOW_TV_INPUT ="show_tv_input"; @@ -390,7 +394,7 @@ public class Utils { throw new IllegalArgumentException("Not an audio track: " + track); } String language = context.getString(R.string.default_language); - if (track.getLanguage() != null) { + if (!TextUtils.isEmpty(track.getLanguage())) { language = new Locale(track.getLanguage()).getDisplayName(); } else { Log.d(TAG, "No language information found for the audio track: " + track); @@ -528,6 +532,13 @@ public class Utils { } /** + * Converts time in milliseconds to a ISO 8061 string. + */ + public static String toIsoDateTimeString(long timeMillis) { + return ISO_8601.format(new Date(timeMillis)); + } + + /** * Returns a {@link String} object which contains the layout information of the {@code view}. */ public static String toRectString(View view) { @@ -565,22 +576,6 @@ public class Utils { } /** - * Checks if this application is running in tests. - * - * <p>{@link android.app.ActivityManager#isRunningInTestHarness} doesn't return {@code true} for - * the usual devices even the application is running in tests. We need to figure it out by - * checking whether the class in tv-tests-common module can be loaded or not. - */ - public static boolean isRunningInTest() { - try { - Class.forName("com.android.tv.testing.Utils"); - return true; - } catch (ClassNotFoundException e) { - return false; - } - } - - /** * Check if the index is valid for the collection, * @param collection the collection * @param index the index position to test @@ -640,6 +635,30 @@ public class Utils { } } + /** + * Returns input ID of {@link UsbTunerTvInputService}. + */ + @Nullable + public static String getUsbTunerInputId(Context context) { + if (!Features.USB_TUNER.isEnabled(context)) { + return null; + } + return TvContract.buildInputId(new ComponentName(context.getPackageName(), + UsbTunerTvInputService.class.getName())); + } + + /** + * Returns {@link TvInputInfo} object of {@link UsbTunerTvInputService}. + */ + @Nullable + public static TvInputInfo getUsbTunerInputInfo(Context context) { + if (!Features.USB_TUNER.isEnabled(context)) { + return null; + } + TvInputManagerHelper helper = TvApplication.getSingletons(context) + .getTvInputManagerHelper(); + return helper.getTvInputInfo(getUsbTunerInputId(context)); + } private static final class SyncRunnable implements Runnable { private final Runnable mTarget; diff --git a/src/com/android/usbtuner/UsbInputController.java b/src/com/android/usbtuner/UsbInputController.java new file mode 100644 index 00000000..7ff82589 --- /dev/null +++ b/src/com/android/usbtuner/UsbInputController.java @@ -0,0 +1,173 @@ +/* + * 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.usbtuner; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import com.android.tv.Features; +import com.android.tv.TvApplication; +import com.android.usbtuner.tvinput.UsbTunerTvInputService; + +import java.util.Map; + +/** + * Controls the package visibility of {@link UsbTunerTvInputService}. + * <p> + * Listens to broadcast intent for {@link Intent#ACTION_BOOT_COMPLETED}, + * {@code UsbManager.ACTION_USB_DEVICE_ATTACHED}, and {@code UsbManager.ACTION_USB_DEVICE_ATTACHED} + * to update the connection status of the supported USB TV tuners. + */ +public class UsbInputController extends BroadcastReceiver { + private static final boolean DEBUG = false; + private static final String TAG = "UsbInputController"; + + private static final TunerDevice[] TUNER_DEVICES = { + new TunerDevice(0x2040, 0xb123), // WinTV-HVR-955Q + new TunerDevice(0x07ca, 0x0837) // AverTV Volar Hybrid Q + }; + + private static final boolean IS_MNC_OR_LATER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; + + private static final int MSG_ENABLE_INPUT_SERVICE = 1000; + private static final long DVB_DRIVER_CHECK_DELAY_MS = 300; + + private DvbDeviceAccessor mDvbDeviceAccessor; + private Handler mHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_ENABLE_INPUT_SERVICE: + Context context = (Context) msg.obj; + if (mDvbDeviceAccessor == null) { + mDvbDeviceAccessor = new DvbDeviceAccessor(context); + } + enableUsbTunerTvInputService(context, + mDvbDeviceAccessor.isDvbDeviceAvailable()); + break; + } + } + }; + + /** + * Simple data holder for a USB device. Used to represent a tuner model, and compare + * against {@link UsbDevice}. + */ + private static class TunerDevice { + private final int vendorId; + private final int productId; + + private TunerDevice(int vendorId, int productId) { + this.vendorId = vendorId; + this.productId = productId; + } + + private boolean equals(UsbDevice device) { + return device.getVendorId() == vendorId && device.getProductId() == productId; + } + } + + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent); + + if (!Features.USB_TUNER.isEnabled(context)) { + enableUsbTunerTvInputService(context, false); + return; + } + + switch (intent.getAction()) { + case Intent.ACTION_BOOT_COMPLETED: + case UsbManager.ACTION_USB_DEVICE_ATTACHED: + case UsbManager.ACTION_USB_DEVICE_DETACHED: + // Tuner is supported on MNC and later version only. + boolean enabled = IS_MNC_OR_LATER && isTunerConnected(context); + mHandler.removeMessages(MSG_ENABLE_INPUT_SERVICE); + if (enabled) { + // Need to check if DVB driver is accessible. Since the driver creation + // could be happen after the USB event, delay the checking by + // DVB_DRIVER_CHECK_DELAY_MS. + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context), + DVB_DRIVER_CHECK_DELAY_MS); + } else { + enableUsbTunerTvInputService(context, false); + } + break; + } + } + + /** + * See if any USB tuner hardware is attached in the system. + * + * @param context {@link Context} instance + * @return {@code true} if any tuner device we support is plugged in + */ + private boolean isTunerConnected(Context context) { + UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + Map<String, UsbDevice> deviceList = manager.getDeviceList(); + for (UsbDevice device : deviceList.values()) { + if (DEBUG) { + Log.d(TAG, "Device: " + device); + } + for (TunerDevice tuner : TUNER_DEVICES) { + if (tuner.equals(device)) { + Log.i(TAG, "Tuner found"); + return true; + } + } + } + return false; + } + + /** + * Enable/disable the component {@link UsbTunerTvInputService}. + * + * @param context {@link Context} instance + * @param enabled {@code true} to enable the service; otherwise {@code false} + */ + private void enableUsbTunerTvInputService(Context context, boolean enabled) { + PackageManager pm = context.getPackageManager(); + ComponentName USBTUNER = new ComponentName(context, UsbTunerTvInputService.class); + + // Since PackageManager.DONT_KILL_APP delays the operation by 10 seconds + // (PackageManagerService.BROADCAST_DELAY), we'd better avoid using it. It is used only + // when the LiveChannels app is active since we don't want to kill the running app. + int flags = ((TvApplication) context.getApplicationContext()).hasMainActivity() + ? PackageManager.DONT_KILL_APP : 0; + int newState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + if (newState != pm.getComponentEnabledSetting(USBTUNER)) { + // Send/cancel the USB tuner TV input setup recommendation card. + TunerSetupActivity.onTvInputEnabled(context, enabled); + + // Enable/disable the USB tuner TV input. + pm.setComponentEnabledSetting(USBTUNER, newState, flags); + if (DEBUG) Log.d(TAG, "Status updated:" + enabled); + } + } +} |