aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorNick Chalko <nchalko@google.com>2015-12-09 13:48:17 -0800
committerNick Chalko <nchalko@google.com>2015-12-11 15:09:19 -0800
commit1abddd9f6225298066094e20a6c29061b6af4590 (patch)
tree97d701f8681cca9939c86e5e61523775d4c13aea /src
parent7d67089aa1e9aa2123c3cd2f386d7019a1544db1 (diff)
downloadTV-1abddd9f6225298066094e20a6c29061b6af4590.tar.gz
Sync to ub-tv-heroes at 1.08.301
source change id If9b64d7bbc6e8f77b360e502d34e5452775c0402 Change-Id: I4ffe87911cb85e54880d1d918d1b8fb7bb8cfb7d
Diffstat (limited to 'src')
-rw-r--r--src/com/android/tv/ApplicationSingletons.java51
-rw-r--r--src/com/android/tv/ChannelTuner.java22
-rw-r--r--src/com/android/tv/Features.java28
-rw-r--r--src/com/android/tv/MainActivity.java332
-rw-r--r--src/com/android/tv/SetupPassthroughActivity.java29
-rw-r--r--src/com/android/tv/TvApplication.java116
-rw-r--r--src/com/android/tv/data/Channel.java35
-rw-r--r--src/com/android/tv/data/Program.java35
-rw-r--r--src/com/android/tv/data/TvInputNewComparator.java47
-rw-r--r--src/com/android/tv/dialog/SafeDismissDialogFragment.java2
-rw-r--r--src/com/android/tv/dvr/BaseDvrDataManager.java81
-rw-r--r--src/com/android/tv/dvr/DvrDataManager.java90
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerImpl.java154
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java139
-rw-r--r--src/com/android/tv/dvr/DvrManager.java104
-rw-r--r--src/com/android/tv/dvr/DvrRecordingService.java119
-rw-r--r--src/com/android/tv/dvr/DvrSessionManager.java51
-rw-r--r--src/com/android/tv/dvr/DvrStartRecordingReceiver.java31
-rw-r--r--src/com/android/tv/dvr/Recording.java410
-rw-r--r--src/com/android/tv/dvr/RecordingTask.java133
-rw-r--r--src/com/android/tv/dvr/Scheduler.java169
-rw-r--r--src/com/android/tv/dvr/SeasonRecording.java35
-rw-r--r--src/com/android/tv/dvr/WritableDvrDataManager.java50
-rw-r--r--src/com/android/tv/dvr/provider/AsyncDvrDbTask.java148
-rw-r--r--src/com/android/tv/dvr/provider/DvrContract.java165
-rw-r--r--src/com/android/tv/dvr/provider/DvrDatabaseHelper.java124
-rw-r--r--src/com/android/tv/guide/ProgramItemView.java46
-rw-r--r--src/com/android/tv/menu/AppLinkCardView.java14
-rw-r--r--src/com/android/tv/menu/ChannelsRowAdapter.java2
-rw-r--r--src/com/android/tv/menu/Menu.java6
-rw-r--r--src/com/android/tv/menu/OptionsRowAdapter.java2
-rw-r--r--src/com/android/tv/onboarding/AppOverviewFragment.java24
-rw-r--r--src/com/android/tv/onboarding/OnboardingActivity.java82
-rw-r--r--src/com/android/tv/onboarding/PagingIndicator.java2
-rw-r--r--src/com/android/tv/onboarding/SetupSourcesFragment.java218
-rw-r--r--src/com/android/tv/onboarding/WelcomeFragment.java714
-rw-r--r--src/com/android/tv/onboarding/WelcomePageFragment.java11
-rw-r--r--src/com/android/tv/receiver/AudioCapabilitiesReceiver.java7
-rw-r--r--src/com/android/tv/receiver/BootCompletedReceiver.java25
-rw-r--r--src/com/android/tv/receiver/PackageIntentsReceiver.java85
-rw-r--r--src/com/android/tv/recommendation/ChannelRecord.java6
-rw-r--r--src/com/android/tv/recommendation/NotificationService.java9
-rw-r--r--src/com/android/tv/recommendation/RecommendationDataManager.java9
-rw-r--r--src/com/android/tv/search/DataManagerSearch.java7
-rw-r--r--src/com/android/tv/search/SearchInterface.java2
-rw-r--r--src/com/android/tv/ui/AppLayerTvView.java5
-rw-r--r--src/com/android/tv/ui/KeypadChannelSwitchView.java2
-rw-r--r--src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java2
-rw-r--r--src/com/android/tv/ui/SelectInputView.java10
-rw-r--r--src/com/android/tv/ui/SetupView.java26
-rw-r--r--src/com/android/tv/ui/TunableTvView.java5
-rw-r--r--src/com/android/tv/ui/TvOverlayManager.java34
-rw-r--r--src/com/android/tv/ui/sidepanel/AboutFragment.java26
-rw-r--r--src/com/android/tv/ui/sidepanel/DeveloperFragment.java114
-rw-r--r--src/com/android/tv/ui/sidepanel/SideFragment.java2
-rw-r--r--src/com/android/tv/util/AsyncDbTask.java25
-rw-r--r--src/com/android/tv/util/Clock.java39
-rw-r--r--src/com/android/tv/util/CollectionUtils.java4
-rw-r--r--src/com/android/tv/util/EngOnlyFeature.java9
-rw-r--r--src/com/android/tv/util/MainThreadExecutor.java2
-rw-r--r--src/com/android/tv/util/NamedThreadFactory.java48
-rw-r--r--src/com/android/tv/util/OnboardingUtils.java5
-rw-r--r--src/com/android/tv/util/SetupUtils.java24
-rw-r--r--src/com/android/tv/util/SoftPreconditions.java15
-rw-r--r--src/com/android/tv/util/TvInputManagerHelper.java11
-rw-r--r--src/com/android/tv/util/Utils.java53
-rw-r--r--src/com/android/usbtuner/UsbInputController.java173
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);
+ }
+ }
+}