aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorYoungsang Cho <youngsang@google.com>2016-05-09 18:52:12 -0700
committerYoungsang Cho <youngsang@google.com>2016-05-09 18:53:20 -0700
commit369b6a409204a9b2a95f7ba575d7c3b7bdc94ab7 (patch)
tree9bd059c5ef3414659427204c753a1389c52966c9 /src
parent4a3a606c96e6cc6e2837cda8fa16c4dcce13774d (diff)
downloadTV-369b6a409204a9b2a95f7ba575d7c3b7bdc94ab7.tar.gz
DO NOT MERGE Sync to joey ub-tv-dev at e7fbaa585b1eb7afec05f05032d2e8d99fb595d4
Bug: 28469968 Change-Id: Ie0d3c74af84777dd8f2e2a79aa0454c7e6a7f0d8
Diffstat (limited to 'src')
-rw-r--r--src/com/android/tv/ChannelTuner.java6
-rw-r--r--src/com/android/tv/Features.java63
-rw-r--r--src/com/android/tv/MainActivity.java584
-rw-r--r--src/com/android/tv/MainActivityWrapper.java4
-rw-r--r--src/com/android/tv/SetupPassthroughActivity.java2
-rw-r--r--src/com/android/tv/TimeShiftManager.java204
-rw-r--r--src/com/android/tv/TvApplication.java65
-rw-r--r--src/com/android/tv/TvOptionsManager.java13
-rw-r--r--src/com/android/tv/analytics/SendConfigInfoRunnable.java4
-rw-r--r--src/com/android/tv/data/Channel.java39
-rw-r--r--src/com/android/tv/data/ChannelDataManager.java19
-rw-r--r--src/com/android/tv/data/GenreItems.java39
-rw-r--r--src/com/android/tv/data/Program.java153
-rw-r--r--src/com/android/tv/data/ProgramDataManager.java49
-rw-r--r--src/com/android/tv/data/StreamInfo.java1
-rw-r--r--src/com/android/tv/data/WatchedHistoryManager.java9
-rw-r--r--src/com/android/tv/data/epg/EpgFetcher.java356
-rw-r--r--src/com/android/tv/data/epg/EpgReader.java53
-rw-r--r--src/com/android/tv/data/epg/StubEpgReader.java53
-rw-r--r--src/com/android/tv/dialog/FullscreenDialogFragment.java5
-rw-r--r--src/com/android/tv/dvr/BaseDvrDataManager.java135
-rw-r--r--src/com/android/tv/dvr/DvrDataManager.java73
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerImpl.java308
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java144
-rw-r--r--src/com/android/tv/dvr/DvrManager.java116
-rw-r--r--src/com/android/tv/dvr/DvrPlayActivity.java10
-rw-r--r--src/com/android/tv/dvr/DvrRecordingService.java20
-rw-r--r--src/com/android/tv/dvr/DvrSessionManager.java117
-rw-r--r--src/com/android/tv/dvr/RecordingTask.java208
-rw-r--r--src/com/android/tv/dvr/ScheduledProgramReaper.java53
-rw-r--r--src/com/android/tv/dvr/ScheduledRecording.java (renamed from src/com/android/tv/dvr/Recording.java)225
-rw-r--r--src/com/android/tv/dvr/Scheduler.java51
-rw-r--r--src/com/android/tv/dvr/SeasonRecording.java2
-rw-r--r--src/com/android/tv/dvr/WritableDvrDataManager.java6
-rw-r--r--src/com/android/tv/dvr/provider/AsyncDvrDbTask.java81
-rw-r--r--src/com/android/tv/dvr/provider/DvrContract.java66
-rw-r--r--src/com/android/tv/dvr/provider/DvrDatabaseHelper.java101
-rw-r--r--src/com/android/tv/dvr/ui/DvrBrowseFragment.java102
-rw-r--r--src/com/android/tv/dvr/ui/DvrDialogFragment.java50
-rw-r--r--src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java73
-rw-r--r--src/com/android/tv/dvr/ui/DvrRecordConflictFragment.java82
-rw-r--r--src/com/android/tv/dvr/ui/DvrRecordDeleteFragment.java48
-rw-r--r--src/com/android/tv/dvr/ui/DvrRecordScheduleFragment.java70
-rw-r--r--src/com/android/tv/dvr/ui/EmptyHolder.java27
-rw-r--r--src/com/android/tv/dvr/ui/EmptyItemPresenter.java66
-rw-r--r--src/com/android/tv/dvr/ui/GridItemPresenter.java165
-rw-r--r--src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java30
-rw-r--r--src/com/android/tv/dvr/ui/RecordedProgramPresenter.java121
-rw-r--r--src/com/android/tv/dvr/ui/RecordedProgramsAdapter.java65
-rw-r--r--src/com/android/tv/dvr/ui/RecordingCardView.java115
-rw-r--r--src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java171
-rw-r--r--src/com/android/tv/dvr/ui/ScheduledRecordingsAdapter.java85
-rw-r--r--src/com/android/tv/dvr/ui/SortedArrayAdapter.java193
-rw-r--r--src/com/android/tv/guide/ProgramGrid.java60
-rw-r--r--src/com/android/tv/guide/ProgramGuide.java19
-rw-r--r--src/com/android/tv/guide/ProgramItemView.java136
-rw-r--r--src/com/android/tv/guide/ProgramListAdapter.java48
-rw-r--r--src/com/android/tv/guide/ProgramManager.java179
-rw-r--r--src/com/android/tv/guide/ProgramRow.java2
-rw-r--r--src/com/android/tv/guide/ProgramTableAdapter.java92
-rw-r--r--src/com/android/tv/guide/TimelineRow.java4
-rw-r--r--src/com/android/tv/menu/ActionCardView.java3
-rw-r--r--src/com/android/tv/menu/BaseCardView.java3
-rw-r--r--src/com/android/tv/menu/ChannelsPosterPrefetcher.java2
-rw-r--r--src/com/android/tv/menu/ChannelsRowAdapter.java63
-rw-r--r--src/com/android/tv/menu/ItemListRowView.java8
-rw-r--r--src/com/android/tv/menu/Menu.java4
-rw-r--r--src/com/android/tv/menu/MenuAction.java7
-rw-r--r--src/com/android/tv/menu/MenuLayoutManager.java18
-rw-r--r--src/com/android/tv/menu/MenuView.java7
-rw-r--r--src/com/android/tv/menu/PlayControlsRowView.java42
-rw-r--r--src/com/android/tv/menu/RecordCardView.java189
-rw-r--r--src/com/android/tv/menu/TvOptionsRowAdapter.java36
-rw-r--r--src/com/android/tv/onboarding/OnboardingActivity.java55
-rw-r--r--src/com/android/tv/onboarding/SetupSourcesFragment.java33
-rw-r--r--src/com/android/tv/onboarding/WelcomeFragment.java82
-rw-r--r--src/com/android/tv/parental/ContentRatingSystem.java13
-rw-r--r--src/com/android/tv/receiver/BootCompletedReceiver.java4
-rw-r--r--src/com/android/tv/receiver/GlobalKeyReceiver.java21
-rw-r--r--src/com/android/tv/receiver/PackageIntentsReceiver.java41
-rw-r--r--src/com/android/tv/recommendation/NotificationService.java8
-rw-r--r--src/com/android/tv/recommendation/RecommendationDataManager.java290
-rw-r--r--src/com/android/tv/recommendation/Recommender.java2
-rw-r--r--src/com/android/tv/recommendation/RoutineWatchEvaluator.java27
-rw-r--r--src/com/android/tv/ui/AppLayerTvView.java5
-rw-r--r--src/com/android/tv/ui/BlockScreenView.java241
-rw-r--r--src/com/android/tv/ui/ChannelBannerView.java187
-rw-r--r--src/com/android/tv/ui/DialogUtils.java61
-rw-r--r--src/com/android/tv/ui/KeypadChannelSwitchView.java2
-rw-r--r--src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java4
-rw-r--r--src/com/android/tv/ui/SelectInputView.java8
-rw-r--r--src/com/android/tv/ui/TunableTvView.java408
-rw-r--r--src/com/android/tv/ui/TvOverlayManager.java23
-rw-r--r--src/com/android/tv/ui/TvTransitionManager.java11
-rw-r--r--src/com/android/tv/ui/TvViewUiManager.java122
-rw-r--r--src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java24
-rw-r--r--src/com/android/tv/ui/sidepanel/DeveloperFragment.java84
-rw-r--r--src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java7
-rw-r--r--src/com/android/tv/ui/sidepanel/SettingsFragment.java17
-rw-r--r--src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java82
-rw-r--r--src/com/android/tv/util/AsyncDbTask.java83
-rw-r--r--src/com/android/tv/util/ImageLoader.java46
-rw-r--r--src/com/android/tv/util/MultiLongSparseArray.java5
-rw-r--r--src/com/android/tv/util/NetworkUtils.java66
-rw-r--r--src/com/android/tv/util/OnboardingUtils.java4
-rw-r--r--src/com/android/tv/util/PipInputManager.java65
-rw-r--r--src/com/android/tv/util/RecurringRunner.java1
-rw-r--r--src/com/android/tv/util/SetupUtils.java51
-rw-r--r--src/com/android/tv/util/SoftPreconditions.java163
-rw-r--r--src/com/android/tv/util/SystemProperties.java7
-rw-r--r--src/com/android/tv/util/TvInputManagerHelper.java1
-rw-r--r--src/com/android/tv/util/Utils.java102
-rw-r--r--src/com/android/usbtuner/UsbInputController.java21
113 files changed, 5734 insertions, 2565 deletions
diff --git a/src/com/android/tv/ChannelTuner.java b/src/com/android/tv/ChannelTuner.java
index 0a000e9b..faa27bbd 100644
--- a/src/com/android/tv/ChannelTuner.java
+++ b/src/com/android/tv/ChannelTuner.java
@@ -22,12 +22,12 @@ import android.net.Uri;
import android.os.Handler;
import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
+import android.util.ArraySet;
import android.util.Log;
-import com.android.tv.common.CollectionUtils;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
-import com.android.tv.util.SoftPreconditions;
import com.android.tv.util.TvInputManagerHelper;
import java.util.ArrayList;
@@ -56,7 +56,7 @@ public class ChannelTuner {
private final Handler mHandler = new Handler();
private final ChannelDataManager mChannelDataManager;
- private final Set<Listener> mListeners = CollectionUtils.createSmallSet();
+ private final Set<Listener> mListeners = new ArraySet<>();
@Nullable
private Channel mCurrentChannel;
private final TvInputManagerHelper mInputManager;
diff --git a/src/com/android/tv/Features.java b/src/com/android/tv/Features.java
index 1a665506..6a78b632 100644
--- a/src/com/android/tv/Features.java
+++ b/src/com/android/tv/Features.java
@@ -16,20 +16,21 @@
package com.android.tv;
+import static com.android.tv.common.feature.EngOnlyFeature.ENG_ONLY_FEATURE;
+import static com.android.tv.common.feature.FeatureUtils.AND;
+import static com.android.tv.common.feature.FeatureUtils.ON;
+import static com.android.tv.common.feature.FeatureUtils.OR;
+
+import android.content.Context;
+import android.os.Build;
import android.support.annotation.VisibleForTesting;
+import android.support.v4.os.BuildCompat;
import com.android.tv.common.feature.Feature;
import com.android.tv.common.feature.GServiceFeature;
import com.android.tv.common.feature.PackageVersionFeature;
import com.android.tv.common.feature.PropertyFeature;
-import com.android.tv.common.feature.SharedPreferencesFeature;
-import com.android.tv.common.feature.TestableFeature;
-
-import static com.android.tv.common.feature.FeatureUtils.AND;
-import static com.android.tv.common.feature.FeatureUtils.ON;
-import static com.android.tv.common.feature.FeatureUtils.OR;
-import static com.android.tv.common.feature.TestableFeature.createTestableFeature;
-import static com.android.tv.common.feature.EngOnlyFeature.ENG_ONLY_FEATURE;
+import com.android.tv.util.PermissionUtils;
/**
* List of {@link Feature} for the Live TV App.
@@ -43,47 +44,65 @@ public final class Features {
* <p>Do not turn this on until the splash screen asking existing users to opt-in is launched.
* See <a href="http://b/20228119">b/20228119</a>
*/
- public static Feature ANALYTICS_OPT_IN = ENG_ONLY_FEATURE;
+ public static final Feature ANALYTICS_OPT_IN = ENG_ONLY_FEATURE;
/**
* Analytics that include sensitive information such as channel or program identifiers.
*
* <p>See <a href="http://b/22062676">b/22062676</a>
*/
- public static Feature ANALYTICS_V2 = AND(ON, ANALYTICS_OPT_IN);
+ public static final Feature ANALYTICS_V2 = AND(ON, ANALYTICS_OPT_IN);
- public static Feature EPG_SEARCH = new PropertyFeature("feature_tv_use_epg_search", false);
+ public static final Feature EPG_SEARCH =
+ new PropertyFeature("feature_tv_use_epg_search", false);
- public static SharedPreferencesFeature USB_TUNER = new SharedPreferencesFeature(
- "usb_tuner", true,
- OR(ENG_ONLY_FEATURE, new GServiceFeature("usbtuner_enabled", false)));
- public static Feature DEVELOPER_OPTION = OR(ENG_ONLY_FEATURE,
- new GServiceFeature("usbtuner_enabled", false));
+ public static final Feature USB_TUNER = new Feature() {
+
+ /**
+ * This is special handling just for USB Tuner.
+ * It does not require any N API's but relies on a improvements in N for AC3 support
+ * After release, change class to this to just be
+ * {@link BuildCompat#isAtLeastN()}.
+ */
+ @Override
+ public boolean isEnabled(Context context) {
+ return Build.VERSION.SDK_INT > Build.VERSION_CODES.M || BuildCompat.isAtLeastN();
+ }
+
+ };
private static final String PLAY_STORE_PACKAGE_NAME = "com.android.vending";
private static final int PLAY_STORE_ZIMA_VERSION_CODE = 80441186;
- private static Feature PLAY_STORE_LINK = new PackageVersionFeature(PLAY_STORE_PACKAGE_NAME,
- PLAY_STORE_ZIMA_VERSION_CODE);
+ private static final Feature PLAY_STORE_LINK =
+ new PackageVersionFeature(PLAY_STORE_PACKAGE_NAME, PLAY_STORE_ZIMA_VERSION_CODE);
- public static Feature ONBOARDING_PLAY_STORE = PLAY_STORE_LINK;
+ public static final Feature ONBOARDING_PLAY_STORE = PLAY_STORE_LINK;
/**
* A flag which indicates that the on-boarding experience is used or not.
*
* <p>See <a href="http://b/24070322">b/24070322</a>
*/
- public static Feature ONBOARDING_EXPERIENCE = ONBOARDING_PLAY_STORE;
+ public static final Feature ONBOARDING_EXPERIENCE = ONBOARDING_PLAY_STORE;
private static final String GSERVICE_KEY_UNHIDE = "live_channels_unhide";
/**
* A flag which indicates that LC app is unhidden even when there is no input.
*/
- public static Feature UNHIDE = AND(ONBOARDING_EXPERIENCE,
- new GServiceFeature(GSERVICE_KEY_UNHIDE, false));
+ public static final Feature UNHIDE = AND(ONBOARDING_EXPERIENCE,
+ OR(new GServiceFeature(GSERVICE_KEY_UNHIDE, false), new Feature() {
+ @Override
+ public boolean isEnabled(Context context) {
+ // If LC app runs as non-system app, we unhide the app.
+ return !PermissionUtils.hasAccessAllEpg(context);
+ }
+ }));
@VisibleForTesting
public static Feature TEST_FEATURE = new PropertyFeature("test_feature", false);
+ public static final Feature FETCH_EPG = new PropertyFeature("live_channels_fetch_epg", false);
+
private Features() {
}
}
diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java
index 99bcb125..78fda42a 100644
--- a/src/com/android/tv/MainActivity.java
+++ b/src/com/android/tv/MainActivity.java
@@ -40,6 +40,7 @@ import android.media.tv.TvContract;
import android.media.tv.TvContract.Channels;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
+import android.media.tv.TvInputManager.TvInputCallback;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView.OnUnhandledInputEventListener;
import android.net.Uri;
@@ -52,6 +53,7 @@ import android.provider.Settings;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.v4.os.BuildCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.Display;
@@ -73,10 +75,12 @@ import com.android.tv.analytics.SendConfigInfoRunnable;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.BuildConfig;
import com.android.tv.common.MemoryManageable;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.TvCommonUtils;
import com.android.tv.common.TvContentRatingCache;
import com.android.tv.common.WeakHandler;
import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.recording.RecordedProgram;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.OnCurrentProgramUpdatedListener;
@@ -89,7 +93,7 @@ import com.android.tv.dialog.SafeDismissDialogFragment;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrPlayActivity;
-import com.android.tv.dvr.Recording;
+import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.menu.Menu;
import com.android.tv.onboarding.OnboardingActivity;
import com.android.tv.parental.ContentRatingsManager;
@@ -125,11 +129,13 @@ import com.android.tv.util.PipInputManager.PipInput;
import com.android.tv.util.RecurringRunner;
import com.android.tv.util.SearchManagerHelper;
import com.android.tv.util.SetupUtils;
-import com.android.tv.util.SoftPreconditions;
import com.android.tv.util.SystemProperties;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.TvSettings;
import com.android.tv.util.TvSettings.PipSound;
+import com.android.usbtuner.UsbTunerPreferences;
+import com.android.usbtuner.setup.TunerSetupActivity;
+import com.android.usbtuner.tvinput.UsbTunerTvInputService;
import com.android.tv.util.TvTrackInfoUtils;
import com.android.tv.util.Utils;
@@ -140,7 +146,6 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
-import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
@@ -268,6 +273,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private MediaSession mMediaSession;
private int mNowPlayingCardWidth;
private int mNowPlayingCardHeight;
+ private final MyOnTuneListener mOnTuneListener = new MyOnTuneListener();
private String mInputIdUnderSetup;
private boolean mIsSetupActivityCalledByPopup;
@@ -281,7 +287,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private boolean mDebugNonFullSizeScreen;
private boolean mActivityResumed;
private boolean mActivityStarted;
- private boolean mLaunchedByLauncher;
+ private boolean mShouldTuneToTunerChannel;
private boolean mUseKeycodeBlacklist;
private boolean mShowLockedChannelsTemporarily;
private boolean mBackKeyPressed;
@@ -290,6 +296,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private boolean mAc3PassthroughSupported;
private boolean mShowNewSourcesFragment = true;
private Uri mRecordingUri;
+ private String mUsbTunerInputId;
+ private boolean mOtherActivityLaunched;
private boolean mIsFilmModeSet;
private float mDefaultRefreshRate;
@@ -323,7 +331,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
// A caller which started this activity. (e.g. TvSearch)
private String mSource;
- private Handler mHandler = new MainActivityHandler(this);
+ private final Handler mHandler = new MainActivityHandler(this);
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
@@ -367,6 +375,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
Channel channel = mTvView.getCurrentChannel();
if (channel != null && channel.getId() == channelId) {
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO);
+ updateMediaSession();
}
}
};
@@ -413,6 +422,19 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
};
private ProgramGuideSearchFragment mSearchFragment;
+ private TvInputCallback mTvInputCallback = new TvInputCallback() {
+ @Override
+ public void onInputAdded(String inputId) {
+ if (mUsbTunerInputId.equals(inputId)
+ && UsbTunerPreferences.shouldShowSetupActivity(MainActivity.this)) {
+ Intent intent = TunerSetupActivity.createSetupActivity(MainActivity.this);
+ startActivity(intent);
+ UsbTunerPreferences.setShouldShowSetupActivity(MainActivity.this, false);
+ SetupUtils.getInstance(MainActivity.this).markAsKnownInput(mUsbTunerInputId);
+ }
+ }
+ };
+
private void applyParentalControlSettings() {
boolean parentalControlEnabled = mTvInputManagerHelper.getParentalControlSettings()
.isParentalControlsEnabled();
@@ -424,12 +446,19 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
protected void onCreate(Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG,"onCreate()");
super.onCreate(savedInstanceState);
-
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
+ && !PermissionUtils.hasAccessAllEpg(this)) {
+ Toast.makeText(this, R.string.msg_not_supported_device, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+ boolean skipToShowOnboarding = getIntent().getAction() == Intent.ACTION_VIEW
+ && TvContract.isChannelUriForPassthroughInput(getIntent().getData());
if (Features.ONBOARDING_EXPERIENCE.isEnabled(this)
- && OnboardingUtils.needToShowOnboarding(this)
+ && OnboardingUtils.needToShowOnboarding(this) && !skipToShowOnboarding
&& !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.
+ // TODO: The onboarding is turned off in test, because tests are broken by the
+ // onboarding. We need to enable the feature for tests later.
startActivity(OnboardingActivity.buildIntent(this, getIntent()));
finish();
return;
@@ -442,6 +471,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
mTracker = tvApplication.getTracker();
mTvInputManagerHelper = tvApplication.getTvInputManagerHelper();
+ mTvInputManagerHelper.addCallback(mTvInputCallback);
+ mUsbTunerInputId = UsbTunerTvInputService.getInputId(this);
mChannelDataManager = tvApplication.getChannelDataManager();
mProgramDataManager = tvApplication.getProgramDataManager();
mProgramDataManager.addOnCurrentProgramUpdatedListener(Channel.INVALID_ID,
@@ -455,7 +486,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mMemoryManageables.add(mProgramDataManager);
mMemoryManageables.add(ImageCache.getInstance());
mMemoryManageables.add(TvContentRatingCache.getInstance());
- if(CommonFeatures.DVR.isEnabled(this)) {
+ if (CommonFeatures.DVR.isEnabled(this) && BuildCompat.isAtLeastN()) {
mDvrManager = tvApplication.getDvrManager();
mDvrDataManager = tvApplication.getDvrDataManager();
}
@@ -502,6 +533,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
new OnCurrentProgramUpdatedListener() {
@Override
public void onCurrentProgramUpdated(long channelId, Program program) {
+ updateMediaSession();
switch (mTimeShiftManager.getLastActionId()) {
case TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND:
case TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD:
@@ -618,6 +650,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mSendConfigInfoRecurringRunner.start();
mChannelStatusRecurringRunner = SendChannelStatusRunnable
.startChannelStatusRecurringRunner(this, mTracker, mChannelDataManager);
+
+ // To avoid not updating Rating systems when changing language.
+ mTvInputManagerHelper.getContentRatingsManager().update();
+
initForTest();
}
@@ -625,7 +661,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
public void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults) {
if (requestCode == PERMISSIONS_REQUEST_READ_TV_LISTINGS) {
- if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ if (grantResults != null && grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // Start reload of dependent data
+ mChannelDataManager.reload();
+ mProgramDataManager.reload();
+
// Restart live channels.
Intent intent = getIntent();
finish();
@@ -719,15 +760,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
protected void onResume() {
if (DEBUG) Log.d(TAG, "onResume()");
super.onResume();
- if (!PermissionUtils.hasAccessAllEpg(this)) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- Toast.makeText(this, R.string.msg_not_supported_device, Toast.LENGTH_LONG).show();
- finish();
- } else if (checkSelfPermission(PERMISSION_READ_TV_LISTINGS)
+ if (!PermissionUtils.hasAccessAllEpg(this)
+ && checkSelfPermission(PERMISSION_READ_TV_LISTINGS)
!= PackageManager.PERMISSION_GRANTED) {
- requestPermissions(new String[]{PERMISSION_READ_TV_LISTINGS},
- PERMISSIONS_REQUEST_READ_TV_LISTINGS);
- }
+ requestPermissions(new String[]{PERMISSION_READ_TV_LISTINGS},
+ PERMISSIONS_REQUEST_READ_TV_LISTINGS);
}
mTracker.sendScreenView(SCREEN_NAME);
@@ -735,6 +772,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mNeedShowBackKeyGuide = true;
mActivityResumed = true;
mShowNewSourcesFragment = true;
+ mOtherActivityLaunched = false;
int result = mAudioManager.requestAudioFocus(MainActivity.this,
AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
mAudioFocusStatus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ?
@@ -798,7 +836,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
mBackKeyPressed = false;
mShowLockedChannelsTemporarily = false;
- mLaunchedByLauncher = false;
+ mShouldTuneToTunerChannel = false;
if (!mVisibleBehind) {
mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
mAudioManager.abandonAudioFocus(this);
@@ -836,7 +874,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private void resumeTvIfNeeded() {
if (DEBUG) Log.d(TAG, "resumeTvIfNeeded()");
if (!mTvView.isPlaying() || mInitChannelUri != null
- || (mLaunchedByLauncher && mChannelTuner.isCurrentChannelPassthrough())) {
+ || (mShouldTuneToTunerChannel && 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);
@@ -1079,10 +1117,15 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
public Channel getCurrentChannel() {
- return mChannelTuner.getCurrentChannel();
+ return mTvView.isRecordingPlayback() ? mTvView.getCurrentChannel()
+ : mChannelTuner.getCurrentChannel();
}
public long getCurrentChannelId() {
+ if (mTvView.isRecordingPlayback()) {
+ Channel channel = mTvView.getCurrentChannel();
+ return channel == null ? Channel.INVALID_ID : channel.getId();
+ }
return mChannelTuner.getCurrentChannelId();
}
@@ -1099,7 +1142,31 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
* If the time shifting is available, it can be a past program.
*/
public Program getCurrentProgram() {
- if (mTimeShiftManager.isAvailable()) {
+ return getCurrentProgram(true);
+ }
+
+ /**
+ * Returns {@code true}, if this view is the recording playback mode.
+ */
+ public boolean isRecordingPlayback() {
+ return mTvView.isRecordingPlayback();
+ }
+
+ /**
+ * Returns the recording which is being played right now.
+ */
+ public RecordedProgram getPlayingRecordedProgram() {
+ return mTvView.getPlayingRecordedProgram();
+ }
+
+ /**
+ * Returns the current program which the user is watching right now.<p>
+ *
+ * @param applyTimeShifted If it is true and the time shifting is available, it can be
+ * a past program.
+ */
+ public Program getCurrentProgram(boolean applyTimeShifted) {
+ if (applyTimeShifted && mTimeShiftManager.isAvailable()) {
return mTimeShiftManager.getCurrentProgram();
}
return mProgramDataManager.getCurrentProgram(getCurrentChannelId());
@@ -1372,7 +1439,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
onKeyUp(keyCode, event);
return true;
}
- mLaunchedByLauncher = intent.getBooleanExtra(Utils.EXTRA_KEY_FROM_LAUNCHER, false);
+ mShouldTuneToTunerChannel = intent.getBooleanExtra(Utils.EXTRA_KEY_FROM_LAUNCHER, false);
mInitChannelUri = null;
String extraAction = intent.getStringExtra(Utils.EXTRA_KEY_ACTION);
@@ -1387,14 +1454,24 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
- if (CommonFeatures.DVR.isEnabled(this)) {
+ if (CommonFeatures.DVR.isEnabled(this) && BuildCompat.isAtLeastN()) {
mRecordingUri = intent.getParcelableExtra(Utils.EXTRA_KEY_RECORDING_URI);
if (mRecordingUri != null) {
return true;
}
}
- if (Intent.ACTION_VIEW.equals(intent.getAction())) {
+ // TODO: remove the checkState once N API is finalized.
+ SoftPreconditions.checkState(TvInputManager.ACTION_SETUP_INPUTS.equals(
+ "android.media.tv.action.SETUP_INPUTS"));
+ if (TvInputManager.ACTION_SETUP_INPUTS.equals(intent.getAction())) {
+ runAfterAttachedToWindow(new Runnable() {
+ @Override
+ public void run() {
+ mOverlayManager.showSetupFragment();
+ }
+ });
+ } else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
try {
mSource = uri.getQueryParameter(Utils.PARAM_SOURCE);
@@ -1416,6 +1493,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (Channels.CONTENT_URI.equals(mInitChannelUri)) {
// Tune to default channel.
mInitChannelUri = null;
+ mShouldTuneToTunerChannel = true;
return true;
}
if ((!Utils.isChannelUriForOneChannel(mInitChannelUri)
@@ -1483,6 +1561,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
private void setVolumeByAudioFocusStatus(TunableTvView tvView) {
+ SoftPreconditions.checkState(tvView == mTvView || tvView == mPipView);
if (tvView.isPlaying()) {
switch (mAudioFocusStatus) {
case AudioManager.AUDIOFOCUS_GAIN:
@@ -1497,6 +1576,15 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
break;
}
}
+ if (tvView == mTvView) {
+ if (mPipView != null && mPipView.isPlaying()) {
+ mPipView.setStreamVolume(AUDIO_MIN_VOLUME);
+ }
+ } else { // tvView == mPipView
+ if (mTvView != null && mTvView.isPlaying()) {
+ mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
+ }
+ }
}
private void stopTv() {
@@ -1601,7 +1689,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mPipView.setMain();
scheduleRestoreMainTvView();
mTvViewUiManager.onPipStart();
- mPipView.setStreamVolume(AUDIO_MIN_VOLUME);
+ setVolumeByAudioFocusStatus();
}
private void scheduleRestoreMainTvView() {
@@ -1633,27 +1721,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
private void playRecording(Uri recordingUri) {
- String inputId = recordingUri.getQueryParameter(Recording.PARAM_INPUT_ID);
- SoftPreconditions.checkNotNull(inputId);
- mTvView.playRecording(inputId, recordingUri, new OnTuneListener() {
- @Override
- public void onTuneFailed(Channel channel) { }
-
- @Override
- public void onUnexpectedStop(Channel channel) { }
-
- @Override
- public void onStreamInfoChanged(StreamInfo info) { }
-
- @Override
- public void onChannelRetuned(Uri channel) { }
-
- @Override
- public void onContentBlocked() { }
-
- @Override
- public void onContentAllowed() { }
- });
+ mTvView.playRecording(recordingUri, mOnTuneListener);
+ mOnTuneListener.onPlayRecording();
+ updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE);
}
private void tune() {
@@ -1668,75 +1738,73 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return;
}
mTunePending = false;
- if (!mChannelTuner.isCurrentChannelPassthrough()
- && mTvInputManagerHelper.getTunerTvInputSize() == 0) {
- Toast.makeText(this, R.string.msg_no_input, Toast.LENGTH_SHORT).show();
- // TODO: Direct the user to a Play Store landing page for TvInputService apps.
- finish();
- return;
- }
- SetupUtils setupUtils = SetupUtils.getInstance(this);
- if (!mChannelTuner.isCurrentChannelPassthrough() && setupUtils.isFirstTune()) {
- if (!mChannelTuner.areAllChannelsLoaded()) {
- // tune() will be called, once all channels are loaded.
- stopTv("tune()", false);
- return;
- }
- if (mChannelDataManager.getChannelCount() > 0) {
- mOverlayManager.showIntroDialog();
- } else if (!Features.ONBOARDING_EXPERIENCE.isEnabled(this)) {
- mOverlayManager.showSetupFragment();
+ final Channel channel = mChannelTuner.getCurrentChannel();
+ if (!mChannelTuner.isCurrentChannelPassthrough()) {
+ if (mTvInputManagerHelper.getTunerTvInputSize() == 0) {
+ Toast.makeText(this, R.string.msg_no_input, Toast.LENGTH_SHORT).show();
+ // TODO: Direct the user to a Play Store landing page for TvInputService apps.
+ finish();
return;
}
- }
- if (!TvCommonUtils.isRunningInTest() && mShowNewSourcesFragment
- && setupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) {
- // Show new channel sources fragment.
- runAfterAttachedToWindow(new Runnable() {
- @Override
- public void run() {
- mOverlayManager.runAfterOverlaysAreClosed(new Runnable() {
- @Override
- public void run() {
- mOverlayManager.showNewSourcesFragment();
- }
- });
+ SetupUtils setupUtils = SetupUtils.getInstance(this);
+ if (setupUtils.isFirstTune()) {
+ if (!mChannelTuner.areAllChannelsLoaded()) {
+ // tune() will be called, once all channels are loaded.
+ stopTv("tune()", false);
+ return;
+ }
+ if (mChannelDataManager.getChannelCount() > 0) {
+ mOverlayManager.showIntroDialog();
+ } else if (!Features.ONBOARDING_EXPERIENCE.isEnabled(this)) {
+ mOverlayManager.showSetupFragment();
+ return;
}
- });
- }
- mShowNewSourcesFragment = false;
- if (!mChannelTuner.isCurrentChannelPassthrough()
- && mChannelTuner.getBrowsableChannelCount() == 0
- && mChannelDataManager.getChannelCount() > 0
- && !mOverlayManager.getSideFragmentManager().isActive()) {
- if (!mChannelTuner.areAllChannelsLoaded()) {
- return;
}
- if (mTvInputManagerHelper.getTunerTvInputSize() == 1) {
- mOverlayManager.getSideFragmentManager().show(new CustomizeChannelListFragment());
- } else {
- showSettingsFragment();
+ if (!TvCommonUtils.isRunningInTest() && mShowNewSourcesFragment
+ && setupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) {
+ // Show new channel sources fragment.
+ runAfterAttachedToWindow(new Runnable() {
+ @Override
+ public void run() {
+ mOverlayManager.runAfterOverlaysAreClosed(new Runnable() {
+ @Override
+ public void run() {
+ mOverlayManager.showNewSourcesFragment();
+ }
+ });
+ }
+ });
}
- return;
- }
- // TODO: need to refactor the following code to put in startTv.
- final Channel channel = mChannelTuner.getCurrentChannel();
- if (channel == null) {
- // There is no channel to tune to.
- stopTv("tune()", false);
- if (!mChannelDataManager.isDbLoadFinished()) {
- // Wait until channel data is loaded in order to know the number of channels.
- // tune() will be retried, once the channel data is loaded.
+ mShowNewSourcesFragment = false;
+ if (mChannelTuner.getBrowsableChannelCount() == 0
+ && mChannelDataManager.getChannelCount() > 0
+ && !mOverlayManager.getSideFragmentManager().isActive()) {
+ if (!mChannelTuner.areAllChannelsLoaded()) {
+ return;
+ }
+ if (mTvInputManagerHelper.getTunerTvInputSize() == 1) {
+ mOverlayManager.getSideFragmentManager().show(
+ new CustomizeChannelListFragment());
+ } else {
+ showSettingsFragment();
+ }
return;
}
- if (mOverlayManager.getSideFragmentManager().isActive()) {
+ // TODO: need to refactor the following code to put in startTv.
+ if (channel == null) {
+ // There is no channel to tune to.
+ stopTv("tune()", false);
+ if (!mChannelDataManager.isDbLoadFinished()) {
+ // Wait until channel data is loaded in order to know the number of channels.
+ // tune() will be retried, once the channel data is loaded.
+ return;
+ }
+ if (mOverlayManager.getSideFragmentManager().isActive()) {
+ return;
+ }
+ mOverlayManager.showSetupFragment();
return;
}
- mOverlayManager.showSetupFragment();
- return;
- }
-
- if (!channel.isPassthrough()) {
setupUtils.onTuned();
if (mTuneParams != null) {
Long initChannelId = mTuneParams.getLong(KEY_INIT_CHANNEL_ID);
@@ -1752,9 +1820,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (!isUnderShrunkenTvView()) {
mLastAllowedRatingForCurrentChannel = null;
}
- final boolean wasUnderShrunkenTvView = isUnderShrunkenTvView();
- final long streamInfoUpdateTimeThresholdMs =
- System.currentTimeMillis() + FIRST_STREAM_INFO_UPDATE_DELAY_MILLIS;
mHandler.removeMessages(MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE);
if (mAccessibilityManager.isEnabled()) {
// For every tune, we need to inform the tuned channel or input to a user,
@@ -1774,105 +1839,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mAccessibilityManager.sendAccessibilityEvent(event);
}
- boolean success = mTvView.tuneTo(channel, mTuneParams, new OnTuneListener() {
- boolean mUnlockAllowedRatingBeforeShrunken = true;
-
- @Override
- public void onUnexpectedStop(Channel channel) {
- stopTv();
- startTv(null);
- }
-
- @Override
- public void onTuneFailed(Channel channel) {
- Log.w(TAG, "Failed to tune to channel " + channel.getId()
- + "@" + channel.getInputId());
- if (mTvView.isFadedOut()) {
- mTvView.removeFadeEffect();
- }
- // TODO: show something to user about this error.
- }
+ boolean success = mTvView.tuneTo(channel, mTuneParams, mOnTuneListener);
+ mOnTuneListener.onTune(channel, isUnderShrunkenTvView());
- @Override
- public void onStreamInfoChanged(StreamInfo info) {
- if (info.isVideoAvailable() && mTuneDurationTimer.isRunning()) {
- mTracker.sendChannelTuneTime(info.getCurrentChannel(),
- mTuneDurationTimer.reset());
- }
- // 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()) {
- if (System.currentTimeMillis() > streamInfoUpdateTimeThresholdMs) {
- updateChannelBannerAndShowIfNeeded(
- UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO);
- } else {
- mHandler.sendMessageDelayed(mHandler.obtainMessage(
- MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE),
- streamInfoUpdateTimeThresholdMs - System.currentTimeMillis());
- }
- }
-
- applyDisplayRefreshRate(info.getVideoFrameRate());
- mTvViewUiManager.updateTvView();
- applyMultiAudio();
- applyClosedCaption();
- // TODO: Send command to TIS with checking the settings in TV and CaptionManager.
- mOverlayManager.getMenu().onStreamInfoChanged();
- if (mTvView.isVideoAvailable()) {
- mTvViewUiManager.fadeInTvView();
- }
- mHandler.removeCallbacks(mRestoreMainViewRunnable);
- restoreMainTvView();
- }
-
- @Override
- public void onChannelRetuned(Uri channel) {
- if (channel == null) {
- return;
- }
- Channel currentChannel =
- mChannelDataManager.getChannel(ContentUris.parseId(channel));
- if (currentChannel == null) {
- Log.e(TAG, "onChannelRetuned is called but can't find a channel with the URI "
- + channel);
- return;
- }
- if (isChannelChangeKeyDownReceived()) {
- // Ignore this message if the user is changing the channel.
- return;
- }
- mChannelTuner.setCurrentChannel(currentChannel);
- mTvView.setCurrentChannel(currentChannel);
- updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE);
- }
-
- @Override
- public void onContentBlocked() {
- mTuneDurationTimer.reset();
- TvContentRating rating = mTvView.getBlockedContentRating();
- // When tuneTo was called while TV view was shrunken, if the channel id is the same
- // with the channel watched before shrunken, we allow the rating which was allowed
- // before.
- if (wasUnderShrunkenTvView && mUnlockAllowedRatingBeforeShrunken
- && mChannelBeforeShrunkenTvView.equals(channel)
- && rating.equals(mAllowedRatingBeforeShrunken)) {
- mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView();
- mTvView.requestUnblockContent(rating);
- }
-
- updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
- mTvViewUiManager.fadeInTvView();
- }
-
- @Override
- public void onContentAllowed() {
- if (!isUnderShrunkenTvView()) {
- mUnlockAllowedRatingBeforeShrunken = false;
- }
- updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
- }
- });
mTuneParams = null;
if (!success) {
Toast.makeText(this, R.string.msg_tune_failed, Toast.LENGTH_SHORT).show();
@@ -1969,6 +1938,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
private void updateProgramPosterArt(Program program, @Nullable Bitmap posterArt) {
+ if (getCurrentChannel() == null) {
+ return;
+ }
if (posterArt != null) {
String cardTitleText = program == null ? null : program.getTitle();
if (TextUtils.isEmpty(cardTitleText)) {
@@ -1995,9 +1967,15 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return;
}
- String cardTitleText = program == null ? null : program.getTitle();
- if (TextUtils.isEmpty(cardTitleText)) {
- cardTitleText = channel.getDisplayName();
+ String cardTitleText;
+ if (channel.isPassthrough()) {
+ TvInputInfo input = getTvInputManagerHelper().getTvInputInfo(channel.getInputId());
+ cardTitleText = Utils.loadLabel(this, input);
+ } else {
+ cardTitleText = program == null ? null : program.getTitle();
+ if (TextUtils.isEmpty(cardTitleText)) {
+ cardTitleText = channel.getDisplayName();
+ }
}
Bitmap posterArt = BitmapFactory.decodeResource(
@@ -2077,7 +2055,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private void updateChannelBannerAndShowIfNeeded(@ChannelBannerUpdateReason int reason) {
if(DEBUG) Log.d(TAG, "updateChannelBannerAndShowIfNeeded(reason=" + reason + ")");
- if (!mChannelTuner.isCurrentChannelPassthrough()) {
+ if (!mChannelTuner.isCurrentChannelPassthrough() || mTvView.isRecordingPlayback()) {
int lockType = ChannelBannerView.LOCK_NONE;
if (mTvView.isScreenBlocked()) {
lockType = ChannelBannerView.LOCK_CHANNEL_INFO;
@@ -2321,6 +2299,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mChannelStatusRecurringRunner.stop();
mChannelStatusRecurringRunner = null;
}
+ if (mTvInputManagerHelper != null) {
+ mTvInputManagerHelper.removeCallback(mTvInputCallback);
+ }
super.onDestroy();
}
@@ -2473,7 +2454,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
public void done(boolean success) {
if (success) {
mLastAllowedRatingForCurrentChannel = rating;
- mTvView.requestUnblockContent(rating);
+ mTvView.unblockContent(rating);
}
}
});
@@ -2500,7 +2481,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW);
}
if (keyCode != KeyEvent.KEYCODE_E) {
- mOverlayManager.showMenu(Menu.REASON_NONE);
+ mOverlayManager.showMenu(mTvView.isRecordingPlayback()
+ ? Menu.REASON_RECORDING_PLAYBACK : Menu.REASON_NONE);
}
return true;
case KeyEvent.KEYCODE_CHANNEL_UP:
@@ -2603,17 +2585,18 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
case KeyEvent.KEYCODE_PROG_YELLOW:
case KeyEvent.KEYCODE_BUTTON_Y:
case KeyEvent.KEYCODE_Y: {
- if (CommonFeatures.DVR.isEnabled(this)) {
+ if (CommonFeatures.DVR.isEnabled(this) && BuildCompat.isAtLeastN()) {
// TODO(DVR) only get finished recordings.
- List<Recording> recordings = mDvrDataManager.getRecordings();
- Log.d(TAG, "Found " + recordings.size() + " recordings");
- if (recordings.isEmpty()) {
+ List<RecordedProgram> recordedPrograms = mDvrDataManager
+ .getRecordedPrograms();
+ Log.d(TAG, "Found " + recordedPrograms.size() + " recordings");
+ if (recordedPrograms.isEmpty()) {
Toast.makeText(this, "No finished recording to play", Toast.LENGTH_LONG)
.show();
} else {
- Recording r = recordings.get(0);
+ RecordedProgram r = recordedPrograms.get(0);
Intent intent = new Intent(this, DvrPlayActivity.class);
- intent.putExtra(Recording.RECORDING_ID_EXTRA, r.getId());
+ intent.putExtra(ScheduledRecording.RECORDING_ID_EXTRA, r.getId());
startActivity(intent);
}
return true;
@@ -2664,6 +2647,20 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
+ @Override
+ public void enterPictureInPictureMode() {
+ // We need to hide overlay first, before moving the activity to PIP. If not, UI will
+ // be shown during PIP stack resizing, because UI and its animation is stuck during
+ // PIP resizing.
+ mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ MainActivity.super.enterPictureInPictureMode();
+ }
+ });
+ }
+
public void togglePipView() {
enablePipView(!mPipEnabled, true);
mOverlayManager.getMenu().update();
@@ -2714,7 +2711,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
// Recover the stream volume of the main TV view, if needed.
if (mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW) {
setVolumeByAudioFocusStatus(mTvView);
- mPipView.setStreamVolume(AUDIO_MIN_VOLUME);
mPipSound = TvSettings.PIP_SOUND_MAIN;
mTvOptionsManager.onPipSoundChanged(mPipSound);
}
@@ -2877,10 +2873,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
if (mPipSound == TvSettings.PIP_SOUND_MAIN) {
setVolumeByAudioFocusStatus(mTvView);
- mPipView.setStreamVolume(AUDIO_MIN_VOLUME);
} else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW
setVolumeByAudioFocusStatus(mPipView);
- mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
}
mPipSwap = !mPipSwap;
mTvOptionsManager.onPipSwapChanged(mPipSwap);
@@ -2896,11 +2890,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
if (mPipSound == TvSettings.PIP_SOUND_MAIN) {
setVolumeByAudioFocusStatus(mPipView);
- mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
mPipSound = TvSettings.PIP_SOUND_PIP_WINDOW;
} else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW
setVolumeByAudioFocusStatus(mTvView);
- mPipView.setStreamVolume(AUDIO_MIN_VOLUME);
mPipSound = TvSettings.PIP_SOUND_MAIN;
}
restoreMainTvView();
@@ -2929,9 +2921,26 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
stopPip();
mVisibleBehind = false;
+ if (!mOtherActivityLaunched && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
+ // Workaround: in M, onStop is not called, even though it should be called after
+ // onVisibleBehindCanceled is called. As a workaround, we call finish().
+ finish();
+ }
super.onVisibleBehindCanceled();
}
+ @Override
+ public void startActivity(Intent intent) {
+ mOtherActivityLaunched = true;
+ super.startActivity(intent);
+ }
+
+ @Override
+ public void startActivityForResult(Intent intent, int requestCode) {
+ mOtherActivityLaunched = true;
+ super.startActivityForResult(intent, requestCode);
+ }
+
public List<TvTrackInfo> getTracks(int type) {
return mTvView.getTracks(type);
}
@@ -3015,10 +3024,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
- return;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
- stringId = R.string.msg_channel_unavailable_weak_signal;
- break;
+ return;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
default:
stringId = R.string.msg_channel_unavailable_unknown;
@@ -3119,4 +3126,123 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return CHANNEL_CHANGE_DELAY_MS_IN_NORMAL_SPEED;
}
}
+
+ private class MyOnTuneListener implements OnTuneListener {
+ boolean mUnlockAllowedRatingBeforeShrunken = true;
+ boolean mWasUnderShrunkenTvView;
+ long mStreamInfoUpdateTimeThresholdMs;
+ Channel mChannel;
+
+ public MyOnTuneListener() { }
+
+ private void onTune(Channel channel, boolean wasUnderShrukenTvView) {
+ mStreamInfoUpdateTimeThresholdMs =
+ System.currentTimeMillis() + FIRST_STREAM_INFO_UPDATE_DELAY_MILLIS;
+ mChannel = channel;
+ mWasUnderShrunkenTvView = wasUnderShrukenTvView;
+ }
+
+ private void onPlayRecording() {
+ mStreamInfoUpdateTimeThresholdMs =
+ System.currentTimeMillis() + FIRST_STREAM_INFO_UPDATE_DELAY_MILLIS;
+ mChannel = null;
+ mWasUnderShrunkenTvView = false;
+ }
+
+ @Override
+ public void onUnexpectedStop(Channel channel) {
+ stopTv();
+ startTv(null);
+ }
+
+ @Override
+ public void onTuneFailed(Channel channel) {
+ Log.w(TAG, "Failed to tune to channel " + channel.getId()
+ + "@" + channel.getInputId());
+ if (mTvView.isFadedOut()) {
+ mTvView.removeFadeEffect();
+ }
+ // TODO: show something to user about this error.
+ }
+
+ @Override
+ public void onStreamInfoChanged(StreamInfo info) {
+ if (info.isVideoAvailable() && mTuneDurationTimer.isRunning()) {
+ mTracker.sendChannelTuneTime(info.getCurrentChannel(),
+ mTuneDurationTimer.reset());
+ }
+ // 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()) {
+ if (System.currentTimeMillis() > mStreamInfoUpdateTimeThresholdMs) {
+ updateChannelBannerAndShowIfNeeded(
+ UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO);
+ } else {
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(
+ MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE),
+ mStreamInfoUpdateTimeThresholdMs - System.currentTimeMillis());
+ }
+ }
+
+ applyDisplayRefreshRate(info.getVideoFrameRate());
+ mTvViewUiManager.updateTvView();
+ applyMultiAudio();
+ applyClosedCaption();
+ // TODO: Send command to TIS with checking the settings in TV and CaptionManager.
+ mOverlayManager.getMenu().onStreamInfoChanged();
+ if (mTvView.isVideoAvailable()) {
+ mTvViewUiManager.fadeInTvView();
+ }
+ mHandler.removeCallbacks(mRestoreMainViewRunnable);
+ restoreMainTvView();
+ }
+
+ @Override
+ public void onChannelRetuned(Uri channel) {
+ if (channel == null) {
+ return;
+ }
+ Channel currentChannel =
+ mChannelDataManager.getChannel(ContentUris.parseId(channel));
+ if (currentChannel == null) {
+ Log.e(TAG, "onChannelRetuned is called but can't find a channel with the URI "
+ + channel);
+ return;
+ }
+ if (isChannelChangeKeyDownReceived()) {
+ // Ignore this message if the user is changing the channel.
+ return;
+ }
+ mChannelTuner.setCurrentChannel(currentChannel);
+ mTvView.setCurrentChannel(currentChannel);
+ updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE);
+ }
+
+ @Override
+ public void onContentBlocked() {
+ mTuneDurationTimer.reset();
+ TvContentRating rating = mTvView.getBlockedContentRating();
+ // When tuneTo was called while TV view was shrunken, if the channel id is the same
+ // with the channel watched before shrunken, we allow the rating which was allowed
+ // before.
+ if (mWasUnderShrunkenTvView && mUnlockAllowedRatingBeforeShrunken
+ && mChannelBeforeShrunkenTvView.equals(mChannel)
+ && rating.equals(mAllowedRatingBeforeShrunken)) {
+ mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView();
+ mTvView.unblockContent(rating);
+ }
+
+ updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
+ mTvViewUiManager.fadeInTvView();
+ }
+
+ @Override
+ public void onContentAllowed() {
+ if (!isUnderShrunkenTvView()) {
+ mUnlockAllowedRatingBeforeShrunken = false;
+ }
+ updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
+ }
+ }
}
diff --git a/src/com/android/tv/MainActivityWrapper.java b/src/com/android/tv/MainActivityWrapper.java
index 94f11864..82e96d14 100644
--- a/src/com/android/tv/MainActivityWrapper.java
+++ b/src/com/android/tv/MainActivityWrapper.java
@@ -20,8 +20,8 @@ import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
+import android.util.ArraySet;
-import com.android.tv.common.CollectionUtils;
import com.android.tv.data.Channel;
import java.util.Set;
@@ -34,7 +34,7 @@ import java.util.Set;
public final class MainActivityWrapper {
private MainActivity mActivity;
- private final Set<OnCurrentChannelChangeListener> mListeners = CollectionUtils.createSmallSet();
+ private final Set<OnCurrentChannelChangeListener> mListeners = new ArraySet<>();
/**
* Returns the current main activity.
diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java
index bdabf25b..e6373505 100644
--- a/src/com/android/tv/SetupPassthroughActivity.java
+++ b/src/com/android/tv/SetupPassthroughActivity.java
@@ -23,9 +23,9 @@ import android.media.tv.TvInputInfo;
import android.os.Bundle;
import android.util.Log;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.TvCommonConstants;
import com.android.tv.util.SetupUtils;
-import com.android.tv.util.SoftPreconditions;
import com.android.tv.util.TvInputManagerHelper;
/**
diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java
index f96464e3..a231c29d 100644
--- a/src/com/android/tv/TimeShiftManager.java
+++ b/src/com/android/tv/TimeShiftManager.java
@@ -28,7 +28,9 @@ import android.util.Log;
import android.util.Range;
import com.android.tv.analytics.Tracker;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.WeakHandler;
+import com.android.tv.common.recording.RecordedProgram;
import com.android.tv.data.Channel;
import com.android.tv.data.OnCurrentProgramUpdatedListener;
import com.android.tv.data.Program;
@@ -179,7 +181,7 @@ public class TimeShiftManager {
tvView.setOnScreenBlockedListener(new TunableTvView.OnScreenBlockingChangedListener() {
@Override
public void onScreenBlockingChanged(boolean blocked) {
- onAvailabilityChanged();
+ mPlayController.onAvailabilityChanged();
}
});
}
@@ -195,7 +197,7 @@ public class TimeShiftManager {
* Checks if the trick play is available for the current channel.
*/
public boolean isAvailable() {
- return mPlayController.isAvailable();
+ return mPlayController.mAvailable;
}
/**
@@ -229,11 +231,6 @@ public class TimeShiftManager {
}
}
- public boolean isPlayForRecording() {
- // TODO: need to find better way to check if it's for recording playback.
- return mPlayController.mRecordEndTimeMs != CURRENT_TIME;
- }
-
/**
* Plays the media.
*
@@ -470,11 +467,18 @@ public class TimeShiftManager {
}
/**
+ * Checks whether the TV is playing the recorded content.
+ */
+ public boolean isRecordingPlayback() {
+ return mPlayController.mRecordingPlayback;
+ }
+
+ /**
* Returns {@code true} if the trick play is available and it's playing to the forward direction
* with normal speed, otherwise {@code false}.
*/
public boolean isNormalPlaying() {
- return mPlayController.isAvailable()
+ return mPlayController.mAvailable
&& mPlayController.mPlayStatus == PLAY_STATUS_PLAYING
&& mPlayController.mPlayDirection == PLAY_DIRECTION_FORWARD
&& mPlayController.mDisplayedPlaySpeed == PLAY_SPEED_1X;
@@ -484,7 +488,7 @@ public class TimeShiftManager {
* Checks if the trick play is available and it's playback status is paused.
*/
public boolean isPaused() {
- return mPlayController.isAvailable() && mPlayController.mPlayStatus == PLAY_STATUS_PAUSED;
+ return mPlayController.mAvailable && mPlayController.mPlayStatus == PLAY_STATUS_PAUSED;
}
/**
@@ -502,8 +506,9 @@ public class TimeShiftManager {
}
void onAvailabilityChanged() {
- mProgramManager.onAvailabilityChanged(mPlayController.isAvailable(),
- mPlayController.getCurrentChannel(), mPlayController.mRecordStartTimeMs);
+ mProgramManager.onAvailabilityChanged(mPlayController.mAvailable,
+ mPlayController.mRecordingPlayback ? null : mPlayController.getCurrentChannel(),
+ mPlayController.mRecordStartTimeMs);
updateActions();
// Availability change notification should be always sent
// even if mNotificationEnabled is false.
@@ -513,7 +518,7 @@ public class TimeShiftManager {
}
void onRecordTimeRangeChanged() {
- if (mPlayController.isAvailable()) {
+ if (mPlayController.mAvailable) {
mProgramManager.onRecordTimeRangeChanged(mPlayController.mRecordStartTimeMs,
mPlayController.mRecordEndTimeMs);
}
@@ -590,7 +595,6 @@ public class TimeShiftManager {
private class PlayController {
private final TunableTvView mTvView;
- private long mPossibleStartTimeMs;
private long mRecordStartTimeMs;
private long mRecordEndTimeMs;
@@ -598,6 +602,8 @@ public class TimeShiftManager {
@PlaySpeed private int mDisplayedPlaySpeed = PLAY_SPEED_1X;
@PlayDirection private int mPlayDirection = PLAY_DIRECTION_FORWARD;
private int mPlaybackSpeed;
+ private boolean mAvailable;
+ private boolean mRecordingPlayback;
/**
* Indicates that the trick play is not playing the current time position.
@@ -613,47 +619,11 @@ public class TimeShiftManager {
mTvView.setTimeShiftListener(new TimeShiftListener() {
@Override
public void onAvailabilityChanged() {
- // Do not send the notifications while the availability is changing,
- // because the variables are in the intermediate state.
- // For example, the current program can be null.
- mNotificationEnabled = false;
- mDisplayedPlaySpeed = PLAY_SPEED_1X;
- mPlaybackSpeed = 1;
- mPlayDirection = PLAY_DIRECTION_FORWARD;
- mIsPlayOffsetChanged = false;
- mPossibleStartTimeMs = System.currentTimeMillis();
- mRecordStartTimeMs = mPossibleStartTimeMs;
- mRecordEndTimeMs = CURRENT_TIME;
- mCurrentPositionMediator.initialize(mPossibleStartTimeMs);
- mHandler.removeMessages(MSG_GET_CURRENT_POSITION);
-
- if (isAvailable()) {
- // When the media availability message has come.
- mPlayController.setPlayStatus(PLAY_STATUS_PLAYING);
- mHandler.sendEmptyMessageDelayed(MSG_GET_CURRENT_POSITION,
- REQUEST_CURRENT_POSITION_INTERVAL);
- } else {
- // When the tune command is sent.
- mPlayController.setPlayStatus(PLAY_STATUS_PAUSED);
- }
- TimeShiftManager.this.onAvailabilityChanged();
- mNotificationEnabled = true;
+ PlayController.this.onAvailabilityChanged();
}
@Override
public void onRecordStartTimeChanged(long recordStartTimeMs) {
- if (mRecordEndTimeMs == CURRENT_TIME &&
- recordStartTimeMs < mPossibleStartTimeMs) {
- // Do not warn in this case because it can happen in normal cases.
- if (DEBUG) {
- Log.d(TAG, "Record start time is less then the time when it became "
- + "available. {availableStartTime="
- + Utils.toTimeString(mPossibleStartTimeMs)
- + ", recordStartTimeMs=" + Utils.toTimeString(recordStartTimeMs)
- + "}");
- }
- recordStartTimeMs = mPossibleStartTimeMs;
- }
if (mRecordStartTimeMs == recordStartTimeMs) {
return;
}
@@ -675,26 +645,48 @@ public class TimeShiftManager {
TimeShiftManager.this.play();
}
}
-
- @Override
- public void onRecordEndTimeChanged(long recordEndTimeMs) {
- if (mRecordEndTimeMs == recordEndTimeMs) {
- return;
- }
- mRecordEndTimeMs = recordEndTimeMs;
- TimeShiftManager.this.onRecordTimeRangeChanged();
-
- if (mPlayStatus == PLAY_STATUS_PLAYING &&
- mRecordEndTimeMs - getCurrentPositionMs()
- < RECORDING_BOUNDARY_THRESHOLD) {
- TimeShiftManager.this.pause();
- }
- }
});
}
- boolean isAvailable() {
- return mTvView.isTimeShiftAvailable() && !mTvView.isScreenBlocked();
+ void onAvailabilityChanged() {
+ boolean newAvailable = mTvView.isTimeShiftAvailable() && !mTvView.isScreenBlocked();
+ if (mAvailable == newAvailable) {
+ return;
+ }
+ mAvailable = newAvailable;
+ // Do not send the notifications while the availability is changing,
+ // because the variables are in the intermediate state.
+ // For example, the current program can be null.
+ mNotificationEnabled = false;
+ mDisplayedPlaySpeed = PLAY_SPEED_1X;
+ mPlaybackSpeed = 1;
+ mPlayDirection = PLAY_DIRECTION_FORWARD;
+ mRecordingPlayback = mTvView.isRecordingPlayback();
+ if (mRecordingPlayback) {
+ RecordedProgram recordedProgram = mTvView.getPlayingRecordedProgram();
+ SoftPreconditions.checkNotNull(recordedProgram);
+ mIsPlayOffsetChanged = true;
+ mRecordStartTimeMs = 0;
+ mRecordEndTimeMs = recordedProgram.getDurationMillis();
+ } else {
+ mIsPlayOffsetChanged = false;
+ mRecordStartTimeMs = System.currentTimeMillis();
+ mRecordEndTimeMs = CURRENT_TIME;
+ }
+ mCurrentPositionMediator.initialize(mRecordStartTimeMs);
+ mHandler.removeMessages(MSG_GET_CURRENT_POSITION);
+
+ if (mAvailable) {
+ // When the media availability message has come.
+ mPlayController.setPlayStatus(PLAY_STATUS_PLAYING);
+ mHandler.sendEmptyMessageDelayed(MSG_GET_CURRENT_POSITION,
+ REQUEST_CURRENT_POSITION_INTERVAL);
+ } else {
+ // When the tune command is sent.
+ mPlayController.setPlayStatus(PLAY_STATUS_PAUSED);
+ }
+ TimeShiftManager.this.onAvailabilityChanged();
+ mNotificationEnabled = true;
}
void handleGetCurrentPosition() {
@@ -855,18 +847,25 @@ public class TimeShiftManager {
private final List<Program> mPrograms = new ArrayList<>();
private final Queue<Range<Long>> mProgramLoadQueue = new LinkedList<>();
private LoadProgramsForCurrentChannelTask mProgramLoadTask = null;
+ private int mEmptyFetchCount = 0;
ProgramManager(ProgramDataManager programDataManager) {
mProgramDataManager = programDataManager;
}
void onAvailabilityChanged(boolean available, Channel channel, long currentPositionMs) {
+ if (DEBUG) {
+ Log.d(TAG, "onAvailabilityChanged(" + available + "+," + channel + ", "
+ + currentPositionMs + ")");
+ }
+
mProgramLoadQueue.clear();
if (mProgramLoadTask != null) {
mProgramLoadTask.cancel(true);
}
mHandler.removeMessages(MSG_PREFETCH_PROGRAM);
mPrograms.clear();
+ mEmptyFetchCount = 0;
mChannel = channel;
if (channel == null || channel.isPassthrough()) {
return;
@@ -1133,17 +1132,37 @@ public class TimeShiftManager {
}
Program lastValidProgram = getLastValidProgram();
if (DEBUG) Log.d(TAG, "Last valid program = " + lastValidProgram);
+ final long delay;
if (lastValidProgram != null) {
- long delay = lastValidProgram.getEndTimeUtcMillis()
+ delay = lastValidProgram.getEndTimeUtcMillis()
- PREFETCH_TIME_OFFSET_FROM_PROGRAM_END - System.currentTimeMillis();
- mHandler.sendEmptyMessageDelayed(MSG_PREFETCH_PROGRAM, delay);
- if (DEBUG) Log.d(TAG, "Scheduling with " + delay + "(ms) delays.");
} else {
- mHandler.sendEmptyMessage(MSG_PREFETCH_PROGRAM);
- if (DEBUG) Log.d(TAG, "Scheduling promptly.");
+ // Since there might not be any program data delay the retry 5 seconds,
+ // then 30 seconds then 5 minutes
+ switch (mEmptyFetchCount) {
+ case 0:
+ delay = 0;
+ break;
+ case 1:
+ delay = TimeUnit.SECONDS.toMillis(5);
+ break;
+ case 2:
+ delay = TimeUnit.SECONDS.toMillis(30);
+ break;
+ default:
+ delay = TimeUnit.MINUTES.toMillis(5);
+ break;
+ }
+ if (DEBUG) {
+ Log.d(TAG,
+ "No last valid program. Already tried " + mEmptyFetchCount + " times");
+ }
}
+ mHandler.sendEmptyMessageDelayed(MSG_PREFETCH_PROGRAM, delay);
+ if (DEBUG) Log.d(TAG, "Scheduling with " + delay + "(ms) delays.");
}
+ // Prefecth programs within PREFETCH_DURATION_FOR_NEXT from now.
private void prefetchPrograms() {
long startTimeMs;
Program lastValidProgram = getLastValidProgram();
@@ -1153,11 +1172,13 @@ public class TimeShiftManager {
startTimeMs = lastValidProgram.getEndTimeUtcMillis();
}
long endTimeMs = System.currentTimeMillis() + PREFETCH_DURATION_FOR_NEXT;
- if (DEBUG) {
- Log.d(TAG, "Prefetch task starts: {startTime=" + Utils.toTimeString(startTimeMs)
- + ", endTime=" + Utils.toTimeString(endTimeMs) + "}");
+ if (startTimeMs <= endTimeMs) {
+ if (DEBUG) {
+ Log.d(TAG, "Prefetch task starts: {startTime=" + Utils.toTimeString(startTimeMs)
+ + ", endTime=" + Utils.toTimeString(endTimeMs) + "}");
+ }
+ mProgramLoadQueue.add(Range.create(startTimeMs, endTimeMs));
}
- mProgramLoadQueue.add(Range.create(startTimeMs, endTimeMs));
startTaskIfNeeded();
}
@@ -1185,8 +1206,8 @@ public class TimeShiftManager {
it.remove();
}
}
- if (programs == null || programs.isEmpty() || mPrograms.isEmpty()) {
- mPrograms.addAll(programs);
+ if (programs == null || programs.isEmpty()) {
+ mEmptyFetchCount++;
if (addDummyPrograms(mPeriod)) {
TimeShiftManager.this.onProgramInfoChanged();
}
@@ -1194,19 +1215,22 @@ public class TimeShiftManager {
startNextLoadingIfNeeded();
return;
}
- removeDummyPrograms();
- removeOverlappedPrograms(programs);
- Program loadedProgram = programs.get(0);
- for (int i = 0; i < mPrograms.size() && !programs.isEmpty(); ++i) {
- Program program = mPrograms.get(i);
- while (program.getStartTimeUtcMillis() > loadedProgram
- .getStartTimeUtcMillis()) {
- mPrograms.add(i++, loadedProgram);
- programs.remove(0);
- if (programs.isEmpty()) {
- break;
+ mEmptyFetchCount = 0;
+ if(!mPrograms.isEmpty()) {
+ removeDummyPrograms();
+ removeOverlappedPrograms(programs);
+ Program loadedProgram = programs.get(0);
+ for (int i = 0; i < mPrograms.size() && !programs.isEmpty(); ++i) {
+ Program program = mPrograms.get(i);
+ while (program.getStartTimeUtcMillis() > loadedProgram
+ .getStartTimeUtcMillis()) {
+ mPrograms.add(i++, loadedProgram);
+ programs.remove(0);
+ if (programs.isEmpty()) {
+ break;
+ }
+ loadedProgram = programs.get(0);
}
- loadedProgram = programs.get(0);
}
}
mPrograms.addAll(programs);
diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java
index 0cac4a3b..ef105c94 100644
--- a/src/com/android/tv/TvApplication.java
+++ b/src/com/android/tv/TvApplication.java
@@ -16,6 +16,7 @@
package com.android.tv;
+import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Application;
import android.content.ComponentName;
@@ -23,14 +24,15 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.media.tv.TvContract;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputManager.TvInputCallback;
+import android.os.Build;
import android.os.Bundle;
import android.os.StrictMode;
-import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.support.annotation.UiThread;
+import android.support.v4.os.BuildCompat;
import android.util.Log;
import android.view.KeyEvent;
@@ -47,14 +49,17 @@ import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.ProgramDataManager;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManagerImpl;
-import com.android.tv.dvr.DvrDataManagerInMemoryImpl;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrRecordingService;
import com.android.tv.dvr.DvrSessionManager;
+import com.android.tv.util.Clock;
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 com.android.usbtuner.UsbTunerPreferences;
+import com.android.usbtuner.setup.TunerSetupActivity;
+import com.android.usbtuner.tvinput.UsbTunerTvInputService;
import java.util.List;
@@ -127,7 +132,7 @@ public class TvApplication extends Application implements ApplicationSingletons
handleInputCountChanged();
}
});
- if (CommonFeatures.DVR.isEnabled(this)) {
+ if (CommonFeatures.DVR.isEnabled(this) && BuildCompat.isAtLeastN()) {
mDvrManager = new DvrManager(this);
//NOTE: DvrRecordingService just keeps running.
DvrRecordingService.startService(this);
@@ -148,6 +153,7 @@ public class TvApplication extends Application implements ApplicationSingletons
}
@Override
+ @TargetApi(Build.VERSION_CODES.N)
public DvrSessionManager getDvrSessionManger() {
if (mDvrSessionManager == null) {
mDvrSessionManager = new DvrSessionManager(this);
@@ -199,16 +205,13 @@ public class TvApplication extends Application implements ApplicationSingletons
/**
* Returns {@link DvrDataManager}.
*/
+ @TargetApi(Build.VERSION_CODES.N)
@Override
public DvrDataManager getDvrDataManager() {
if (mDvrDataManager == null) {
- if(SystemProperties.USE_IN_MEMORY_DVR_DB.getValue()){
- mDvrDataManager = new DvrDataManagerInMemoryImpl(this);
- } else {
- DvrDataManagerImpl dvrDataManager = new DvrDataManagerImpl(this);
+ DvrDataManagerImpl dvrDataManager = new DvrDataManagerImpl(this, Clock.SYSTEM);
mDvrDataManager = dvrDataManager;
dvrDataManager.start();
- }
}
return mDvrDataManager;
}
@@ -256,7 +259,9 @@ public class TvApplication extends Application implements ApplicationSingletons
boolean hasTunerInput = false;
for (TvInputInfo input : tvInputs) {
if (input.isPassthroughInput()) {
- ++inputCount;
+ if (!input.isHidden(this)) {
+ ++inputCount;
+ }
} else if (!hasTunerInput) {
hasTunerInput = true;
++inputCount;
@@ -310,17 +315,38 @@ public class TvApplication extends Application implements ApplicationSingletons
* {@link SetupUtils}.
*/
public void handleInputCountChanged() {
+ handleInputCountChanged(false, false, false);
+ }
+
+ /**
+ * Checks the input counts and enable/disable TvActivity. Also updates the input list in
+ * {@link SetupUtils}.
+ *
+ * @param calledByTunerServiceChanged true if it is called when UsbTunerTvInputService
+ * is enabled or disabled.
+ * @param tunerServiceEnabled it's available only when calledByTunerServiceChanged is true.
+ * @param dontKillApp when TvActivity is enabled or disabled by this method, the app restarts
+ * by default. But, if dontKillApp is true, the app won't restart.
+ */
+ public void handleInputCountChanged(boolean calledByTunerServiceChanged,
+ boolean tunerServiceEnabled, boolean dontKillApp) {
TvInputManager inputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE);
- boolean enable = false;
- if (Features.UNHIDE.isEnabled(TvApplication.this)) {
- enable = true;
- } else {
+ boolean enable = (calledByTunerServiceChanged && tunerServiceEnabled)
+ || Features.UNHIDE.isEnabled(TvApplication.this);
+ if (!enable) {
List<TvInputInfo> inputs = inputManager.getTvInputList();
+ boolean skipTunerInputCheck = false;
// Enable the TvActivity only if there is at least one tuner type input.
- for (TvInputInfo input : inputs) {
- if (input.getType() == TvInputInfo.TYPE_TUNER) {
- enable = true;
- break;
+ if (!skipTunerInputCheck) {
+ for (TvInputInfo input : inputs) {
+ if (calledByTunerServiceChanged && !tunerServiceEnabled
+ && UsbTunerTvInputService.getInputId(this).equals(input.getId())) {
+ continue;
+ }
+ if (input.getType() == TvInputInfo.TYPE_TUNER) {
+ enable = true;
+ break;
+ }
}
}
if (DEBUG) Log.d(TAG, "Enable MainActivity: " + enable);
@@ -330,7 +356,8 @@ public class TvApplication extends Application implements ApplicationSingletons
int newState = enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
if (packageManager.getComponentEnabledSetting(name) != newState) {
- packageManager.setComponentEnabledSetting(name, newState, 0);
+ packageManager.setComponentEnabledSetting(name, newState,
+ dontKillApp ? PackageManager.DONT_KILL_APP : 0);
}
SetupUtils.getInstance(TvApplication.this).onInputListUpdated(inputManager);
}
diff --git a/src/com/android/tv/TvOptionsManager.java b/src/com/android/tv/TvOptionsManager.java
index 97b9d5fa..f104e75d 100644
--- a/src/com/android/tv/TvOptionsManager.java
+++ b/src/com/android/tv/TvOptionsManager.java
@@ -35,10 +35,11 @@ import java.util.Locale;
public class TvOptionsManager {
public static final int OPTION_CLOSED_CAPTIONS = 0;
public static final int OPTION_DISPLAY_MODE = 1;
- public static final int OPTION_PIP = 2;
- public static final int OPTION_MULTI_AUDIO = 3;
- public static final int OPTION_MORE_CHANNELS = 4;
- public static final int OPTION_SETTINGS = 5;
+ public static final int OPTION_IN_APP_PIP = 2;
+ public static final int OPTION_SYSTEMWIDE_PIP = 3;
+ public static final int OPTION_MULTI_AUDIO = 4;
+ public static final int OPTION_MORE_CHANNELS = 5;
+ public static final int OPTION_SETTINGS = 6;
public static final int OPTION_PIP_INPUT = 100;
public static final int OPTION_PIP_SWAP = 101;
@@ -75,7 +76,7 @@ public class TvOptionsManager {
.isDisplayModeAvailable(mDisplayMode)
? DisplayMode.getLabel(mDisplayMode, mContext)
: DisplayMode.getLabel(DisplayMode.MODE_NORMAL, mContext);
- case OPTION_PIP:
+ case OPTION_IN_APP_PIP:
return mContext.getString(
mPip ? R.string.options_item_pip_on : R.string.options_item_pip_off);
case OPTION_MULTI_AUDIO:
@@ -130,7 +131,7 @@ public class TvOptionsManager {
public void onPipChanged(boolean pip) {
mPip = pip;
- notifyOptionChanged(OPTION_PIP);
+ notifyOptionChanged(OPTION_IN_APP_PIP);
}
public void onMultiAudioChanged(String multiAudio) {
diff --git a/src/com/android/tv/analytics/SendConfigInfoRunnable.java b/src/com/android/tv/analytics/SendConfigInfoRunnable.java
index c2d5c5fb..41392a6d 100644
--- a/src/com/android/tv/analytics/SendConfigInfoRunnable.java
+++ b/src/com/android/tv/analytics/SendConfigInfoRunnable.java
@@ -26,8 +26,8 @@ import java.util.List;
* Sends ConfigurationInfo once a day.
*/
public class SendConfigInfoRunnable implements Runnable {
- private Tracker mTracker;
- private TvInputManagerHelper mTvInputManagerHelper;
+ private final Tracker mTracker;
+ private final TvInputManagerHelper mTvInputManagerHelper;
public SendConfigInfoRunnable(Tracker tracker, TvInputManagerHelper tvInputManagerHelper) {
this.mTracker = tracker;
diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java
index ba3c59ba..86437ab2 100644
--- a/src/com/android/tv/data/Channel.java
+++ b/src/com/android/tv/data/Channel.java
@@ -33,7 +33,6 @@ import android.util.Log;
import com.android.tv.common.CollectionUtils;
import com.android.tv.common.TvCommonConstants;
-import com.android.tv.dvr.provider.DvrContract;
import com.android.tv.util.ImageLoader;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -75,17 +74,17 @@ public final class Channel {
private static final String INVALID_PACKAGE_NAME = "packageName";
private static final String[] PROJECTION_BASE = {
- // Columns must match what is read in Channel.fromCursor()
- TvContract.Channels._ID,
- TvContract.Channels.COLUMN_PACKAGE_NAME,
- TvContract.Channels.COLUMN_INPUT_ID,
- TvContract.Channels.COLUMN_TYPE,
- TvContract.Channels.COLUMN_DISPLAY_NUMBER,
- TvContract.Channels.COLUMN_DISPLAY_NAME,
- TvContract.Channels.COLUMN_DESCRIPTION,
- TvContract.Channels.COLUMN_VIDEO_FORMAT,
- TvContract.Channels.COLUMN_BROWSABLE,
- TvContract.Channels.COLUMN_LOCKED,
+ // Columns must match what is read in Channel.fromCursor()
+ TvContract.Channels._ID,
+ TvContract.Channels.COLUMN_PACKAGE_NAME,
+ TvContract.Channels.COLUMN_INPUT_ID,
+ TvContract.Channels.COLUMN_TYPE,
+ TvContract.Channels.COLUMN_DISPLAY_NUMBER,
+ TvContract.Channels.COLUMN_DISPLAY_NAME,
+ TvContract.Channels.COLUMN_DESCRIPTION,
+ TvContract.Channels.COLUMN_VIDEO_FORMAT,
+ TvContract.Channels.COLUMN_BROWSABLE,
+ TvContract.Channels.COLUMN_LOCKED,
};
// Additional fields added in MNC.
@@ -110,15 +109,6 @@ 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}
@@ -264,6 +254,13 @@ public final class Channel {
}
/**
+ * Checks whether this channel is physical tuner channel or not.
+ */
+ public boolean isPhysicalTunerChannel() {
+ return !TextUtils.isEmpty(mType) && !TvContract.Channels.TYPE_OTHER.equals(mType);
+ }
+
+ /**
* Checks if two channels equal by checking ids.
*/
@Override
diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java
index 82ac4b5a..84a16111 100644
--- a/src/com/android/tv/data/ChannelDataManager.java
+++ b/src/com/android/tv/data/ChannelDataManager.java
@@ -31,15 +31,15 @@ import android.os.Message;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
+import android.util.ArraySet;
import android.util.Log;
import android.util.MutableInt;
-import com.android.tv.common.CollectionUtils;
import com.android.tv.common.SharedPreferencesUtils;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.WeakHandler;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.PermissionUtils;
-import com.android.tv.util.SoftPreconditions;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -72,7 +72,7 @@ public class ChannelDataManager {
private QueryAllChannelsTask mChannelsUpdateTask;
private final List<Runnable> mPostRunnablesAfterChannelUpdate = new ArrayList<>();
- private final Set<Listener> mListeners = CollectionUtils.createSmallSet();
+ private final Set<Listener> mListeners = new ArraySet<>();
private final Map<Long, ChannelWrapper> mChannelWrapperMap = new HashMap<>();
private final Map<String, MutableInt> mChannelCountMap = new HashMap<>();
private final Channel.DefaultComparator mChannelComparator;
@@ -282,7 +282,7 @@ public class ChannelDataManager {
channels.add(channel);
}
}
- return Collections.unmodifiableList(channels);
+ return channels;
}
/**
@@ -508,6 +508,15 @@ public class ChannelDataManager {
mChannelsUpdateTask.executeOnDbThread();
}
+ /**
+ * Reloads channel data.
+ */
+ public void reload() {
+ if (mDbLoadFinished && !mHandler.hasMessages(MSG_UPDATE_CHANNELS)) {
+ mHandler.sendEmptyMessage(MSG_UPDATE_CHANNELS);
+ }
+ }
+
public interface Listener {
/**
* Called when data load is finished.
@@ -539,7 +548,7 @@ public class ChannelDataManager {
}
private class ChannelWrapper {
- final Set<ChannelListener> mChannelListeners = CollectionUtils.createSmallSet();
+ final Set<ChannelListener> mChannelListeners = new ArraySet<>();
final Channel mChannel;
boolean mBrowsableInDb;
boolean mLockedInDb;
diff --git a/src/com/android/tv/data/GenreItems.java b/src/com/android/tv/data/GenreItems.java
index 92e38809..b1110612 100644
--- a/src/com/android/tv/data/GenreItems.java
+++ b/src/com/android/tv/data/GenreItems.java
@@ -17,13 +17,11 @@
package com.android.tv.data;
import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
import android.content.Context;
import android.media.tv.TvContract.Programs.Genres;
import android.os.Build;
import com.android.tv.R;
-import com.android.tv.common.CollectionUtils;
public class GenreItems {
/**
@@ -31,7 +29,7 @@ public class GenreItems {
*/
public static final int ID_ALL_CHANNELS = 0;
- private static final String[] CANONICAL_GENRES_BASE = {
+ private static final String[] CANONICAL_GENRES_L = {
null, // All channels
Genres.FAMILY_KIDS,
Genres.SPORTS,
@@ -47,23 +45,34 @@ public class GenreItems {
};
@SuppressLint("InlinedApi")
- private static final String[] CANONICAL_GENRES_ADDED_IN_L_MR1 = {
- Genres.ARTS,
- Genres.ENTERTAINMENT,
- Genres.LIFE_STYLE,
- Genres.MUSIC,
- Genres.PREMIER,
- Genres.TECH_SCIENCE
+ private static final String[] CANONICAL_GENRES_L_MR1 = {
+ null, // All channels
+ Genres.FAMILY_KIDS,
+ Genres.SPORTS,
+ Genres.SHOPPING,
+ Genres.MOVIES,
+ Genres.COMEDY,
+ Genres.TRAVEL,
+ Genres.DRAMA,
+ Genres.EDUCATION,
+ Genres.ANIMAL_WILDLIFE,
+ Genres.NEWS,
+ Genres.GAMING,
+ Genres.ARTS,
+ Genres.ENTERTAINMENT,
+ Genres.LIFE_STYLE,
+ Genres.MUSIC,
+ Genres.PREMIER,
+ Genres.TECH_SCIENCE
};
private static final String[] CANONICAL_GENRES = createGenres();
private static String[] createGenres() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
- return CANONICAL_GENRES_BASE;
+ return CANONICAL_GENRES_L;
} else {
- return CollectionUtils
- .concatAll(CANONICAL_GENRES_BASE, CANONICAL_GENRES_ADDED_IN_L_MR1);
+ return CANONICAL_GENRES_L_MR1;
}
}
@@ -73,7 +82,9 @@ public class GenreItems {
* Returns array of all genre labels.
*/
public static String[] getLabels(Context context) {
- String[] items = context.getResources().getStringArray(R.array.genre_labels);
+ String[] items = Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1
+ ? context.getResources().getStringArray(R.array.genre_labels_l)
+ : context.getResources().getStringArray(R.array.genre_labels_l_mr1);
if (items.length != CANONICAL_GENRES.length) {
throw new IllegalArgumentException("Genre data mismatch");
}
diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/Program.java
index b9c54aac..af5f93bb 100644
--- a/src/com/android/tv/data/Program.java
+++ b/src/com/android/tv/data/Program.java
@@ -22,13 +22,14 @@ import android.media.tv.TvContentRating;
import android.media.tv.TvContract;
import android.support.annotation.NonNull;
import android.support.annotation.UiThread;
+import android.support.v4.os.BuildCompat;
import android.text.TextUtils;
import android.util.Log;
import com.android.tv.R;
import com.android.tv.common.BuildConfig;
+import com.android.tv.common.CollectionUtils;
import com.android.tv.common.TvContentRatingCache;
-import com.android.tv.dvr.provider.DvrContract;
import com.android.tv.util.ImageLoader;
import com.android.tv.util.Utils;
@@ -43,33 +44,43 @@ public final class Program implements Comparable<Program> {
private static final boolean DEBUG_DUMP_DESCRIPTION = false;
private static final String TAG = "Program";
- public static final String[] PROJECTION = {
- // Columns must match what is read in Program.fromCursor()
- TvContract.Programs.COLUMN_CHANNEL_ID,
- TvContract.Programs.COLUMN_TITLE,
- TvContract.Programs.COLUMN_EPISODE_TITLE,
- TvContract.Programs.COLUMN_SEASON_NUMBER,
- TvContract.Programs.COLUMN_EPISODE_NUMBER,
- TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
- TvContract.Programs.COLUMN_POSTER_ART_URI,
- TvContract.Programs.COLUMN_THUMBNAIL_URI,
- TvContract.Programs.COLUMN_CANONICAL_GENRE,
- TvContract.Programs.COLUMN_CONTENT_RATING,
- TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
- TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
- TvContract.Programs.COLUMN_VIDEO_WIDTH,
- TvContract.Programs.COLUMN_VIDEO_HEIGHT
+ private static final String[] PROJECTION_BASE = {
+ // Columns must match what is read in Program.fromCursor()
+ TvContract.Programs._ID,
+ TvContract.Programs.COLUMN_CHANNEL_ID,
+ TvContract.Programs.COLUMN_TITLE,
+ TvContract.Programs.COLUMN_EPISODE_TITLE,
+ TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
+ TvContract.Programs.COLUMN_POSTER_ART_URI,
+ TvContract.Programs.COLUMN_THUMBNAIL_URI,
+ TvContract.Programs.COLUMN_CANONICAL_GENRE,
+ TvContract.Programs.COLUMN_CONTENT_RATING,
+ TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
+ TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
+ TvContract.Programs.COLUMN_VIDEO_WIDTH,
+ TvContract.Programs.COLUMN_VIDEO_HEIGHT
};
- /**
- * 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
+ // Columns which is deprecated in NYC
+ private static final String[] PROJECTION_DEPRECATED_IN_NYC = {
+ TvContract.Programs.COLUMN_SEASON_NUMBER,
+ TvContract.Programs.COLUMN_EPISODE_NUMBER
+ };
+
+ private static final String[] PROJECTION_ADDED_IN_NYC = {
+ TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER,
+ TvContract.Programs.COLUMN_SEASON_TITLE,
+ TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER
};
+ public static final String[] PROJECTION = createProjection();
+
+ private static String[] createProjection() {
+ return CollectionUtils
+ .concatAll(PROJECTION_BASE, BuildCompat.isAtLeastN() ? PROJECTION_ADDED_IN_NYC
+ : PROJECTION_DEPRECATED_IN_NYC);
+ }
+
/**
* Creates {@code Program} object from cursor.
*
@@ -79,39 +90,38 @@ public final class Program implements Comparable<Program> {
// Columns read must match the order of match {@link #PROJECTION}
Builder builder = new Builder();
int index = 0;
+ builder.setId(cursor.getLong(index++));
builder.setChannelId(cursor.getLong(index++));
builder.setTitle(cursor.getString(index++));
builder.setEpisodeTitle(cursor.getString(index++));
- builder.setSeasonNumber(cursor.getInt(index++));
- builder.setEpisodeNumber(cursor.getInt(index++));
builder.setDescription(cursor.getString(index++));
builder.setPosterArtUri(cursor.getString(index++));
builder.setThumbnailUri(cursor.getString(index++));
builder.setCanonicalGenres(cursor.getString(index++));
- builder.setContentRatings(TvContentRatingCache.getInstance()
- .getRatings(cursor.getString(index++)));
+ builder.setContentRatings(
+ TvContentRatingCache.getInstance().getRatings(cursor.getString(index++)));
builder.setStartTimeUtcMillis(cursor.getLong(index++));
builder.setEndTimeUtcMillis(cursor.getLong(index++));
builder.setVideoWidth((int) cursor.getLong(index++));
builder.setVideoHeight((int) cursor.getLong(index++));
+ if (BuildCompat.isAtLeastN()) {
+ builder.setSeasonNumber(cursor.getString(index++));
+ builder.setSeasonTitle(cursor.getString(index++));
+ builder.setEpisodeNumber(cursor.getString(index++));
+ } else {
+ builder.setSeasonNumber(cursor.getString(index++));
+ builder.setEpisodeNumber(cursor.getString(index++));
+ }
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 mId;
private long mChannelId;
private String mTitle;
private String mEpisodeTitle;
- private int mSeasonNumber;
- private int mEpisodeNumber;
+ private String mSeasonNumber;
+ private String mSeasonTitle;
+ private String mEpisodeNumber;
private long mStartTimeUtcMillis;
private long mEndTimeUtcMillis;
private String mDescription;
@@ -122,8 +132,6 @@ public final class Program implements Comparable<Program> {
private int[] mCanonicalGenreIds;
private TvContentRating[] mContentRatings;
- private long mDvrId;
-
/**
* TODO(DVR): Need to fill the following data.
*/
@@ -134,6 +142,10 @@ public final class Program implements Comparable<Program> {
// Do nothing.
}
+ public long getId() {
+ return mId;
+ }
+
public long getChannelId() {
return mChannelId;
}
@@ -161,13 +173,22 @@ public final class Program implements Comparable<Program> {
}
public String getEpisodeDisplayTitle(Context context) {
- if (mSeasonNumber > 0 && mEpisodeNumber > 0 && !TextUtils.isEmpty(mEpisodeTitle)) {
+ if (!TextUtils.isEmpty(mSeasonNumber) && !TextUtils.isEmpty(mEpisodeNumber)
+ && !TextUtils.isEmpty(mEpisodeTitle)) {
return String.format(context.getResources().getString(R.string.episode_format),
mSeasonNumber, mEpisodeNumber, mEpisodeTitle);
}
return mEpisodeTitle;
}
+ public String getSeasonNumber() {
+ return mSeasonNumber;
+ }
+
+ public String getEpisodeNumber() {
+ return mEpisodeNumber;
+ }
+
public long getStartTimeUtcMillis() {
return mStartTimeUtcMillis;
}
@@ -239,19 +260,12 @@ 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,
mTitle, mEpisodeTitle, mDescription, mVideoWidth, mVideoHeight,
mPosterArtUri, mThumbnailUri, Arrays.hashCode(mContentRatings),
- Arrays.hashCode(mCanonicalGenreIds), mSeasonNumber, mEpisodeNumber);
+ Arrays.hashCode(mCanonicalGenreIds), mSeasonNumber, mSeasonTitle, mEpisodeNumber);
}
@Override
@@ -272,8 +286,9 @@ public final class Program implements Comparable<Program> {
&& Objects.equals(mThumbnailUri, program.mThumbnailUri)
&& Arrays.equals(mContentRatings, program.mContentRatings)
&& Arrays.equals(mCanonicalGenreIds, program.mCanonicalGenreIds)
- && mSeasonNumber == program.mSeasonNumber
- && mEpisodeNumber == program.mEpisodeNumber;
+ && Objects.equals(mSeasonNumber, program.mSeasonNumber)
+ && Objects.equals(mSeasonTitle, program.mSeasonTitle)
+ && Objects.equals(mEpisodeNumber, program.mEpisodeNumber);
}
@Override
@@ -284,11 +299,12 @@ public final class Program implements Comparable<Program> {
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
- builder.append("Program{")
+ builder.append("Program[" + mId + "]{")
.append("channelId=").append(mChannelId)
.append(", title=").append(mTitle)
.append(", episodeTitle=").append(mEpisodeTitle)
.append(", seasonNumber=").append(mSeasonNumber)
+ .append(", seasonTitle=").append(mSeasonTitle)
.append(", episodeNumber=").append(mEpisodeNumber)
.append(", startTimeUtcSec=").append(Utils.toTimeString(mStartTimeUtcMillis))
.append(", endTimeUtcSec=").append(Utils.toTimeString(mEndTimeUtcMillis))
@@ -310,10 +326,12 @@ public final class Program implements Comparable<Program> {
return;
}
+ mId = other.mId;
mChannelId = other.mChannelId;
mTitle = other.mTitle;
mEpisodeTitle = other.mEpisodeTitle;
mSeasonNumber = other.mSeasonNumber;
+ mSeasonTitle = other.mSeasonTitle;
mEpisodeNumber = other.mEpisodeNumber;
mStartTimeUtcMillis = other.mStartTimeUtcMillis;
mEndTimeUtcMillis = other.mEndTimeUtcMillis;
@@ -328,17 +346,19 @@ public final class Program implements Comparable<Program> {
public static final class Builder {
private final Program mProgram;
+ private long mId;
public Builder() {
mProgram = new Program();
// Fill initial data.
mProgram.mChannelId = Channel.INVALID_ID;
- mProgram.mTitle = "title";
- mProgram.mSeasonNumber = -1;
- mProgram.mEpisodeNumber = -1;
+ mProgram.mTitle = null;
+ mProgram.mSeasonNumber = null;
+ mProgram.mSeasonTitle = null;
+ mProgram.mEpisodeNumber = null;
mProgram.mStartTimeUtcMillis = -1;
mProgram.mEndTimeUtcMillis = -1;
- mProgram.mDescription = "description";
+ mProgram.mDescription = null;
}
public Builder(Program other) {
@@ -346,6 +366,11 @@ public final class Program implements Comparable<Program> {
mProgram.copyFrom(other);
}
+ public Builder setId(long id) {
+ mProgram.mId = id;
+ return this;
+ }
+
public Builder setChannelId(long channelId) {
mProgram.mChannelId = channelId;
return this;
@@ -361,12 +386,17 @@ public final class Program implements Comparable<Program> {
return this;
}
- public Builder setSeasonNumber(int seasonNumber) {
+ public Builder setSeasonNumber(String seasonNumber) {
mProgram.mSeasonNumber = seasonNumber;
return this;
}
- public Builder setEpisodeNumber(int episodeNumber) {
+ public Builder setSeasonTitle(String seasonTitle) {
+ mProgram.mSeasonTitle = seasonTitle;
+ return this;
+ }
+
+ public Builder setEpisodeNumber(String episodeNumber) {
mProgram.mEpisodeNumber = episodeNumber;
return this;
}
@@ -473,11 +503,10 @@ public final class Program implements Comparable<Program> {
boolean isDuplicate = p1.getChannelId() == p2.getChannelId()
&& p1.getStartTimeUtcMillis() == p2.getStartTimeUtcMillis()
&& p1.getEndTimeUtcMillis() == p2.getEndTimeUtcMillis();
- if (BuildConfig.ENG && isDuplicate) {
+ if (DEBUG && BuildConfig.ENG && isDuplicate) {
Log.w(TAG, "Duplicate programs detected! - \"" + p1.getTitle() + "\" and \""
+ p2.getTitle() + "\"");
}
return isDuplicate;
}
-
}
diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java
index 6c167238..88db91b9 100644
--- a/src/com/android/tv/data/ProgramDataManager.java
+++ b/src/com/android/tv/data/ProgramDataManager.java
@@ -28,16 +28,17 @@ import android.os.Looper;
import android.os.Message;
import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
+import android.util.ArraySet;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.LruCache;
-import com.android.tv.common.CollectionUtils;
import com.android.tv.common.MemoryManageable;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.data.epg.EpgFetcher;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.Clock;
import com.android.tv.util.MultiLongSparseArray;
-import com.android.tv.util.SoftPreconditions;
import com.android.tv.util.Utils;
import java.util.ArrayList;
@@ -90,7 +91,7 @@ public class ProgramDataManager implements MemoryManageable {
private final MultiLongSparseArray<OnCurrentProgramUpdatedListener>
mChannelId2ProgramUpdatedListeners = new MultiLongSparseArray<>();
private final Handler mHandler;
- private final Set<Listener> mListeners = CollectionUtils.createSmallSet();
+ private final Set<Listener> mListeners = new ArraySet<>();
private final ContentObserver mProgramObserver;
@@ -108,8 +109,12 @@ public class ProgramDataManager implements MemoryManageable {
private boolean mPauseProgramUpdate = false;
private final LruCache<Long, Program> mZeroLengthProgramCache = new LruCache<>(10);
+ // TODO: Change to final.
+ private EpgFetcher mEpgFetcher;
+
public ProgramDataManager(Context context) {
this(context.getContentResolver(), Clock.SYSTEM, Looper.myLooper());
+ mEpgFetcher = new EpgFetcher(context);
}
@VisibleForTesting
@@ -128,8 +133,8 @@ public class ProgramDataManager implements MemoryManageable {
}
if (mPrefetchEnabled) {
// The delay time of an existing MSG_UPDATE_PREFETCH_PROGRAM could be quite long
- // up to PROGRAM_GUIDE_SNAP_TIME_MS. So we need to remove the existing message and
- // send MSG_UPDATE_PREFETCH_PROGRAM again.
+ // up to PROGRAM_GUIDE_SNAP_TIME_MS. So we need to remove the existing message
+ // and send MSG_UPDATE_PREFETCH_PROGRAM again.
mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM);
mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
}
@@ -169,6 +174,9 @@ public class ProgramDataManager implements MemoryManageable {
}
mContentResolver.registerContentObserver(Programs.CONTENT_URI,
true, mProgramObserver);
+ if (mEpgFetcher != null) {
+ mEpgFetcher.start();
+ }
}
/**
@@ -182,6 +190,9 @@ public class ProgramDataManager implements MemoryManageable {
}
mStarted = false;
+ if (mEpgFetcher != null) {
+ mEpgFetcher.stop();
+ }
mContentResolver.unregisterContentObserver(mProgramObserver);
mHandler.removeCallbacksAndMessages(null);
@@ -201,6 +212,18 @@ public class ProgramDataManager implements MemoryManageable {
}
/**
+ * Reloads program data.
+ */
+ public void reload() {
+ if (!mHandler.hasMessages(MSG_UPDATE_CURRENT_PROGRAMS)) {
+ mHandler.sendEmptyMessage(MSG_UPDATE_CURRENT_PROGRAMS);
+ }
+ if (mPrefetchEnabled && !mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) {
+ mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
+ }
+ }
+
+ /**
* A listener interface to receive notification on program data retrieval from DB.
*/
public interface Listener {
@@ -601,6 +624,22 @@ public class ProgramDataManager implements MemoryManageable {
}
}
+ /**
+ * Gets an single {@link Program} from {@link TvContract.Programs#CONTENT_URI}.
+ */
+ public static class QueryProgramTask extends AsyncDbTask.AsyncQueryItemTask<Program> {
+
+ public QueryProgramTask(ContentResolver contentResolver, long programId) {
+ super(contentResolver, TvContract.buildProgramUri(programId), Program.PROJECTION, null,
+ null, null);
+ }
+
+ @Override
+ protected Program fromCursor(Cursor c) {
+ return Program.fromCursor(c);
+ }
+ }
+
private class MyHandler extends Handler {
public MyHandler(Looper looper) {
super(looper);
diff --git a/src/com/android/tv/data/StreamInfo.java b/src/com/android/tv/data/StreamInfo.java
index 04f8258a..df842737 100644
--- a/src/com/android/tv/data/StreamInfo.java
+++ b/src/com/android/tv/data/StreamInfo.java
@@ -30,6 +30,7 @@ public interface StreamInfo {
int getVideoWidth();
int getVideoHeight();
float getVideoFrameRate();
+ float getVideoDisplayAspectRatio();
int getVideoDefinitionLevel();
int getAudioChannelCount();
boolean hasClosedCaption();
diff --git a/src/com/android/tv/data/WatchedHistoryManager.java b/src/com/android/tv/data/WatchedHistoryManager.java
index cff8cd5c..fc6672d2 100644
--- a/src/com/android/tv/data/WatchedHistoryManager.java
+++ b/src/com/android/tv/data/WatchedHistoryManager.java
@@ -3,10 +3,12 @@ package com.android.tv.data;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
@@ -42,8 +44,8 @@ public class WatchedHistoryManager {
private boolean mStarted;
private boolean mLoaded;
private SharedPreferences mSharedPreferences;
- private SharedPreferences.OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener =
- new SharedPreferences.OnSharedPreferenceChangeListener() {
+ private final OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener =
+ new OnSharedPreferenceChangeListener() {
@Override
@MainThread
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
@@ -80,7 +82,7 @@ public class WatchedHistoryManager {
private final Context mContext;
private Listener mListener;
private final int mMaxHistorySize;
- private Handler mHandler;
+ private final Handler mHandler;
public WatchedHistoryManager(Context context) {
this(context, MAX_HISTORY_SIZE);
@@ -197,6 +199,7 @@ public class WatchedHistoryManager {
* Returns watched history in the ascending order of time. In other words, the first element
* is the oldest and the last element is the latest record.
*/
+ @NonNull
public List<WatchedRecord> getWatchedHistory() {
return Collections.unmodifiableList(mWatchedHistory);
}
diff --git a/src/com/android/tv/data/epg/EpgFetcher.java b/src/com/android/tv/data/epg/EpgFetcher.java
new file mode 100644
index 00000000..9ff527d8
--- /dev/null
+++ b/src/com/android/tv/data/epg/EpgFetcher.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.data.epg;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.media.tv.TvContract.Programs;
+import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputManager.TvInputCallback;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.tv.Features;
+import com.android.tv.TvApplication;
+import com.android.tv.common.WeakHandler;
+import com.android.tv.data.Channel;
+import com.android.tv.data.Program;
+import com.android.tv.util.RecurringRunner;
+import com.android.tv.util.TvInputManagerHelper;
+import com.android.tv.util.Utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An utility class to fetch the EPG. This class isn't thread-safe.
+ */
+public class EpgFetcher {
+ private static final String TAG = "EpgFetcher";
+ private static final boolean DEBUG = false;
+
+ private static final int MSG_FETCH_EPG = 1;
+
+ private static final long EPG_PREFETCH_RECURRING_PERIOD_MS = TimeUnit.HOURS.toMillis(4);
+ private static final long EPG_READER_INIT_WAIT_MS = TimeUnit.MINUTES.toMillis(1);
+ private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(30);
+
+ private static final int BATCH_OPERATION_COUNT = 100;
+
+ // Value: Long
+ private static final String KEY_LAST_UPDATED_EPG_TIMESTAMP =
+ "com.android.tv.data.epg.EpgFetcher.LastUpdatedEpgTimestamp";
+
+ private final Context mContext;
+ private final TvInputManagerHelper mInputHelper;
+ private final TvInputCallback mInputCallback;
+ private HandlerThread mHandlerThread;
+ private EpgFetcherHandler mHandler;
+ private RecurringRunner mRecurringRunner;
+
+ private long mLastEpgTimestamp = -1;
+
+ public EpgFetcher(Context context) {
+ mContext = context;
+ mInputHelper = TvApplication.getSingletons(mContext).getTvInputManagerHelper();
+ mInputCallback = new TvInputCallback() {
+ @Override
+ public void onInputAdded(String inputId) {
+ if (Utils.isInternalTvInput(mContext, inputId)) {
+ mHandler.removeMessages(MSG_FETCH_EPG);
+ mHandler.sendEmptyMessage(MSG_FETCH_EPG);
+ }
+ }
+ };
+ }
+
+ /**
+ * Starts fetching EPG.
+ */
+ public void start() {
+ if (DEBUG) Log.d(TAG, "Request to start fetching EPG.");
+ if (!Features.FETCH_EPG.isEnabled(mContext)) {
+ return;
+ }
+ if (mHandlerThread == null) {
+ mHandlerThread = new HandlerThread("EpgFetcher");
+ mHandlerThread.start();
+ mHandler = new EpgFetcherHandler(mHandlerThread.getLooper(), this);
+ mInputHelper.addCallback(mInputCallback);
+ mRecurringRunner = new RecurringRunner(mContext, EPG_PREFETCH_RECURRING_PERIOD_MS,
+ new Runnable() {
+ @Override
+ public void run() {
+ mHandler.removeMessages(MSG_FETCH_EPG);
+ mHandler.sendEmptyMessage(MSG_FETCH_EPG);
+ }
+ }, null);
+ mRecurringRunner.start();
+ }
+ }
+
+ /**
+ * Stops fetching EPG.
+ */
+ public void stop() {
+ if (mHandlerThread == null) {
+ return;
+ }
+ mRecurringRunner.stop();
+ mHandler.removeCallbacksAndMessages(null);
+ mHandler = null;
+ mHandlerThread.quit();
+ mHandlerThread = null;
+ }
+
+ private void onFetchEpg() {
+ if (DEBUG) Log.d(TAG, "Start fetching EPG.");
+ // Check for the internal inputs.
+ boolean hasInternalInput = false;
+ for (TvInputInfo input : mInputHelper.getTvInputInfos(true, true)) {
+ if (Utils.isInternalTvInput(mContext, input.getId())) {
+ hasInternalInput = true;
+ break;
+ }
+ }
+ if (!hasInternalInput) {
+ if (DEBUG) Log.d(TAG, "No internal input found.");
+ return;
+ }
+ // Check if EPG reader is available.
+ EpgReader epgReader = new StubEpgReader(mContext);
+ if (!epgReader.isAvailable()) {
+ if (DEBUG) Log.d(TAG, "EPG reader is not temporarily available.");
+ mHandler.removeMessages(MSG_FETCH_EPG);
+ mHandler.sendEmptyMessageDelayed(MSG_FETCH_EPG, EPG_READER_INIT_WAIT_MS);
+ return;
+ }
+ // Check the EPG Timestamp.
+ long epgTimestamp = epgReader.getEpgTimestamp();
+ if (epgTimestamp <= getLastUpdatedEpgTimestamp()) {
+ if (DEBUG) Log.d(TAG, "No new EPG.");
+ return;
+ }
+
+ List<Channel> channels = epgReader.getChannels();
+ for (Channel channel : channels) {
+ List<Program> programs = new ArrayList<>(epgReader.getPrograms(channel.getId()));
+ Collections.sort(programs);
+ if (DEBUG) {
+ Log.d(TAG, "Fetching " + programs.size() + " programs for channel " + channel);
+ }
+ updateEpg(channel.getId(), programs);
+ }
+
+ setLastUpdatedEpgTimestamp(epgTimestamp);
+ }
+
+ private long getLastUpdatedEpgTimestamp() {
+ if (mLastEpgTimestamp < 0) {
+ mLastEpgTimestamp = PreferenceManager.getDefaultSharedPreferences(mContext).getLong(
+ KEY_LAST_UPDATED_EPG_TIMESTAMP, 0);
+ }
+ return mLastEpgTimestamp;
+ }
+
+ private void setLastUpdatedEpgTimestamp(long timestamp) {
+ mLastEpgTimestamp = timestamp;
+ PreferenceManager.getDefaultSharedPreferences(mContext).edit().putLong(
+ KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp);
+ }
+
+ private void updateEpg(long channelId, List<Program> newPrograms) {
+ final int fetchedProgramsCount = newPrograms.size();
+ if (fetchedProgramsCount == 0) {
+ return;
+ }
+ long startTimeMs = System.currentTimeMillis();
+ long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION;
+ List<Program> oldPrograms = queryPrograms(mContext.getContentResolver(), channelId,
+ startTimeMs, endTimeMs);
+ Program currentOldProgram = oldPrograms.size() > 0 ? oldPrograms.get(0) : null;
+ int oldProgramsIndex = 0;
+ int newProgramsIndex = 0;
+ // Skip the past programs. They will be automatically removed by the system.
+ if (currentOldProgram != null) {
+ long oldStartTimeUtcMillis = currentOldProgram.getStartTimeUtcMillis();
+ for (Program program : newPrograms) {
+ if (program.getEndTimeUtcMillis() > oldStartTimeUtcMillis) {
+ break;
+ }
+ newProgramsIndex++;
+ }
+ }
+ // Compare the new programs with old programs one by one and update/delete the old one
+ // or insert new program if there is no matching program in the database.
+ ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ while (newProgramsIndex < fetchedProgramsCount) {
+ // TODO: Extract to method and make test.
+ Program oldProgram = oldProgramsIndex < oldPrograms.size()
+ ? oldPrograms.get(oldProgramsIndex) : null;
+ Program newProgram = newPrograms.get(newProgramsIndex);
+ boolean addNewProgram = false;
+ if (oldProgram != null) {
+ if (oldProgram.equals(newProgram)) {
+ // Exact match. No need to update. Move on to the next programs.
+ oldProgramsIndex++;
+ newProgramsIndex++;
+ } else if (isSameTitleAndOverlap(oldProgram, newProgram)) {
+ if (!oldProgram.equals(oldProgram)) {
+ // Partial match. Update the old program with the new one.
+ // NOTE: Use 'update' in this case instead of 'insert' and 'delete'. There
+ // could be application specific settings which belong to the old program.
+ ops.add(ContentProviderOperation.newUpdate(
+ TvContract.buildProgramUri(oldProgram.getId()))
+ .withValues(toContentValues(newProgram))
+ .build());
+ }
+ oldProgramsIndex++;
+ newProgramsIndex++;
+ } else if (oldProgram.getEndTimeUtcMillis()
+ < newProgram.getEndTimeUtcMillis()) {
+ // No match. Remove the old program first to see if the next program in
+ // {@code oldPrograms} partially matches the new program.
+ ops.add(ContentProviderOperation.newDelete(
+ TvContract.buildProgramUri(oldProgram.getId()))
+ .build());
+ oldProgramsIndex++;
+ } else {
+ // No match. The new program does not match any of the old programs. Insert
+ // it as a new program.
+ addNewProgram = true;
+ newProgramsIndex++;
+ }
+ } else {
+ // No old programs. Just insert new programs.
+ addNewProgram = true;
+ newProgramsIndex++;
+ }
+ if (addNewProgram) {
+ ops.add(ContentProviderOperation
+ .newInsert(TvContract.Programs.CONTENT_URI)
+ .withValues(toContentValues(newProgram))
+ .build());
+ }
+ // Throttle the batch operation not to cause TransactionTooLargeException.
+ if (ops.size() > BATCH_OPERATION_COUNT || newProgramsIndex >= fetchedProgramsCount) {
+ try {
+ if (DEBUG) {
+ int size = ops.size();
+ Log.d(TAG, "Running " + size + " operations for channel " + channelId);
+ for (int i = 0; i < size; ++i) {
+ Log.d(TAG, "Operation(" + i + "): " + ops.get(i));
+ }
+ }
+ mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.e(TAG, "Failed to insert programs.", e);
+ return;
+ }
+ ops.clear();
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Fetched " + fetchedProgramsCount + " programs for channel " + channelId);
+ }
+ }
+
+ private List<Program> queryPrograms(ContentResolver contentResolver, long channelId,
+ long startTimeMs, long endTimeMs) {
+ try (Cursor c = mContext.getContentResolver().query(
+ TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs),
+ Program.PROJECTION, null, null, Programs.COLUMN_START_TIME_UTC_MILLIS)) {
+ if (c == null) {
+ return Collections.EMPTY_LIST;
+ }
+ ArrayList<Program> programs = new ArrayList<>();
+ while (c.moveToNext()) {
+ programs.add(Program.fromCursor(c));
+ }
+ return programs;
+ }
+ }
+
+ /**
+ * Returns {@code true} if the {@code oldProgram} program needs to be updated with the
+ * {@code newProgram} program.
+ */
+ private boolean isSameTitleAndOverlap(Program oldProgram, Program newProgram) {
+ // NOTE: Here, we update the old program if it has the same title and overlaps with the
+ // new program. The test logic is just an example and you can modify this. E.g. check
+ // whether the both programs have the same program ID if your EPG supports any ID for
+ // the programs.
+ return Objects.equals(oldProgram.getTitle(), newProgram.getTitle())
+ && oldProgram.getStartTimeUtcMillis() <= newProgram.getEndTimeUtcMillis()
+ && newProgram.getStartTimeUtcMillis() <= oldProgram.getEndTimeUtcMillis();
+ }
+
+ private static ContentValues toContentValues(Program program) {
+ ContentValues values = new ContentValues();
+ values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId());
+ putValue(values, TvContract.Programs.COLUMN_TITLE, program.getTitle());
+ putValue(values, TvContract.Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle());
+ putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber());
+ putValue(values, TvContract.Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber());
+ putValue(values, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription());
+ putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri());
+ values.put(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
+ program.getStartTimeUtcMillis());
+ values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis());
+ return values;
+ }
+
+ private static void putValue(ContentValues contentValues, String key, String value) {
+ if (TextUtils.isEmpty(value)) {
+ contentValues.putNull(key);
+ } else {
+ contentValues.put(key, value);
+ }
+ }
+
+ private static class EpgFetcherHandler extends WeakHandler<EpgFetcher> {
+ public EpgFetcherHandler (@NonNull Looper looper, EpgFetcher ref) {
+ super(looper, ref);
+ }
+
+ @Override
+ public void handleMessage(Message msg, @NonNull EpgFetcher epgFetcher) {
+ switch (msg.what) {
+ case MSG_FETCH_EPG:
+ epgFetcher.onFetchEpg();
+ break;
+ default:
+ super.handleMessage(msg);
+ break;
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java
new file mode 100644
index 00000000..1c7712f4
--- /dev/null
+++ b/src/com/android/tv/data/epg/EpgReader.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.data.epg;
+
+import android.support.annotation.WorkerThread;
+
+import com.android.tv.data.Channel;
+import com.android.tv.data.Program;
+
+import java.util.List;
+
+/**
+ * An interface used to retrieve the EPG data. This class should be used in worker thread.
+ */
+@WorkerThread
+public interface EpgReader {
+ /**
+ * Checks if the reader is available.
+ */
+ boolean isAvailable();
+
+ /**
+ * Returns the timestamp of the current EPG.
+ * The format should be YYYYMMDDHHmmSS as a long value. ex) 20160308141500
+ */
+ long getEpgTimestamp();
+
+ /**
+ * Returns the channels list.
+ */
+ List<Channel> getChannels();
+
+ /**
+ * Returns the programs for the given channel. The result is sorted by the start time.
+ * Note that the {@code Program} doesn't have valid program ID because it's not retrieved from
+ * TvProvider.
+ */
+ List<Program> getPrograms(long channelId);
+}
diff --git a/src/com/android/tv/data/epg/StubEpgReader.java b/src/com/android/tv/data/epg/StubEpgReader.java
new file mode 100644
index 00000000..2896e8e5
--- /dev/null
+++ b/src/com/android/tv/data/epg/StubEpgReader.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.data.epg;
+
+import android.content.Context;
+
+import com.android.tv.data.Channel;
+import com.android.tv.data.Program;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A stub class to read EPG.
+ */
+public class StubEpgReader implements EpgReader{
+ public StubEpgReader(Context context) {
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public long getEpgTimestamp() {
+ return 0;
+ }
+
+ @Override
+ public List<Channel> getChannels() {
+ return Collections.EMPTY_LIST;
+ }
+
+ @Override
+ public List<Program> getPrograms(long channelId) {
+ return Collections.EMPTY_LIST;
+ }
+}
diff --git a/src/com/android/tv/dialog/FullscreenDialogFragment.java b/src/com/android/tv/dialog/FullscreenDialogFragment.java
index eb84aaf9..d16202a1 100644
--- a/src/com/android/tv/dialog/FullscreenDialogFragment.java
+++ b/src/com/android/tv/dialog/FullscreenDialogFragment.java
@@ -48,7 +48,6 @@ public class FullscreenDialogFragment extends SafeDismissDialogFragment {
return f;
}
- private int mViewLayoutResId;
private String mTrackerLabel;
private DialogView mDialogView;
@@ -58,9 +57,9 @@ public class FullscreenDialogFragment extends SafeDismissDialogFragment {
new FullscreenDialog(getActivity(), R.style.Theme_TV_dialog_Fullscreen);
LayoutInflater inflater = LayoutInflater.from(getActivity());
Bundle args = getArguments();
- mViewLayoutResId = args.getInt(VIEW_LAYOUT_ID);
mTrackerLabel = args.getString(TRACKER_LABEL);
- View v = inflater.inflate(mViewLayoutResId, null);
+ int viewLayoutResId = args.getInt(VIEW_LAYOUT_ID);
+ View v = inflater.inflate(viewLayoutResId, null);
dialog.setContentView(v);
mDialogView = (DialogView) v;
mDialogView.initialize((MainActivity) getActivity(), dialog);
diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java
index a98b5fa0..0fb469be 100644
--- a/src/com/android/tv/dvr/BaseDvrDataManager.java
+++ b/src/com/android/tv/dvr/BaseDvrDataManager.java
@@ -16,68 +16,155 @@
package com.android.tv.dvr;
+import android.annotation.TargetApi;
import android.content.Context;
+import android.os.Build;
import android.support.annotation.MainThread;
+import android.util.ArraySet;
import android.util.Log;
-import com.android.tv.common.CollectionUtils;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.util.SoftPreconditions;
+import com.android.tv.common.recording.RecordedProgram;
+import com.android.tv.util.Clock;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Set;
/**
* Base implementation of @{link DataManagerInternal}.
*/
@MainThread
+@TargetApi(Build.VERSION_CODES.N)
public abstract class BaseDvrDataManager implements WritableDvrDataManager {
private final static String TAG = "BaseDvrDataManager";
private final static boolean DEBUG = false;
+ protected final Clock mClock;
- private final Set<DvrDataManager.Listener> mListeners = CollectionUtils.createSmallSet();
+ private final Set<ScheduledRecordingListener> mScheduledRecordingListeners = new ArraySet<>();
+ private final Set<RecordedProgramListener> mRecordedProgramListeners = new ArraySet<>();
- BaseDvrDataManager (Context context){
+ BaseDvrDataManager(Context context, Clock clock) {
SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG);
+ mClock = clock;
}
@Override
- public final void addListener(DvrDataManager.Listener listener) {
- mListeners.add(listener);
+ public final void addScheduledRecordingListener(ScheduledRecordingListener listener) {
+ mScheduledRecordingListeners.add(listener);
}
@Override
- public final void removeListener(DvrDataManager.Listener listener) {
- mListeners.remove(listener);
+ public final void removeScheduledRecordingListener(ScheduledRecordingListener listener) {
+ mScheduledRecordingListeners.remove(listener);
+ }
+
+ @Override
+ public final void addRecordedProgramListener(RecordedProgramListener listener) {
+ mRecordedProgramListeners.add(listener);
+ }
+
+ @Override
+ public final void removeRecordedProgramListener(RecordedProgramListener listener) {
+ mRecordedProgramListeners.remove(listener);
}
/**
- * Calls {@link DvrDataManager.Listener#onRecordingAdded(Recording)} for each current listener.
+ * Calls {@link RecordedProgramListener#onRecordedProgramAdded(RecordedProgram)}
+ * for each listener.
*/
- protected final void notifyRecordingAdded(Recording recording) {
- for (Listener l : mListeners) {
- if (DEBUG) Log.d(TAG, "notify " + l + "added recording " + recording);
- l.onRecordingAdded(recording);
+ protected final void notifyRecordedProgramAdded(RecordedProgram recordedProgram) {
+ for (RecordedProgramListener l : mRecordedProgramListeners) {
+ if (DEBUG) Log.d(TAG, "notify " + l + "added " + recordedProgram);
+ l.onRecordedProgramAdded(recordedProgram);
}
}
/**
- * Calls {@link DvrDataManager.Listener#onRecordingRemoved(Recording)} for each current listener.
+ * Calls {@link RecordedProgramListener#onRecordedProgramChanged(RecordedProgram)}
+ * for each listener.
*/
- protected final void notifyRecordingRemoved(Recording recording) {
- for (Listener l : mListeners) {
- if (DEBUG) Log.d(TAG, "notify " + l + "removed recording " + recording);
- l.onRecordingRemoved(recording);
+ protected final void notifyRecordedProgramChanged(RecordedProgram recordedProgram) {
+ for (RecordedProgramListener l : mRecordedProgramListeners) {
+ if (DEBUG) Log.d(TAG, "notify " + l + "changed " + recordedProgram);
+ l.onRecordedProgramChanged(recordedProgram);
}
}
/**
- * Calls {@link DvrDataManager.Listener#onRecordingStatusChanged(Recording)} for each current
- * listener.
+ * Calls {@link RecordedProgramListener#onRecordedProgramRemoved(RecordedProgram)}
+ * for each listener.
*/
- protected final void notifyRecordingStatusChanged(Recording recording) {
- for (Listener l : mListeners) {
- if (DEBUG) Log.d(TAG, "notify " + l + "changed recording " + recording);
- l.onRecordingStatusChanged(recording);
+ protected final void notifyRecordedProgramRemoved(RecordedProgram recordedProgram) {
+ for (RecordedProgramListener l : mRecordedProgramListeners) {
+ if (DEBUG) Log.d(TAG, "notify " + l + "removed " + recordedProgram);
+ l.onRecordedProgramRemoved(recordedProgram);
}
}
+
+ /**
+ * Calls {@link ScheduledRecordingListener#onScheduledRecordingAdded(ScheduledRecording)}
+ * for each listener.
+ */
+ protected final void notifyScheduledRecordingAdded(ScheduledRecording scheduledRecording) {
+ for (ScheduledRecordingListener l : mScheduledRecordingListeners) {
+ if (DEBUG) Log.d(TAG, "notify " + l + "added " + scheduledRecording);
+ l.onScheduledRecordingAdded(scheduledRecording);
+ }
+ }
+
+ /**
+ * Calls {@link ScheduledRecordingListener#onScheduledRecordingRemoved(ScheduledRecording)}
+ * for each listener.
+ */
+ protected final void notifyScheduledRecordingRemoved(ScheduledRecording scheduledRecording) {
+ for (ScheduledRecordingListener l : mScheduledRecordingListeners) {
+ if (DEBUG) {
+ Log.d(TAG, "notify " + l + "removed " + scheduledRecording);
+ }
+ l.onScheduledRecordingRemoved(scheduledRecording);
+ }
+ }
+
+ /**
+ * Calls
+ * {@link ScheduledRecordingListener#onScheduledRecordingStatusChanged(ScheduledRecording)}
+ * for each listener.
+ */
+ protected final void notifyScheduledRecordingStatusChanged(
+ ScheduledRecording scheduledRecording) {
+ for (ScheduledRecordingListener l : mScheduledRecordingListeners) {
+ if (DEBUG) Log.d(TAG, "notify " + l + "changed " + scheduledRecording);
+ l.onScheduledRecordingStatusChanged(scheduledRecording);
+ }
+ }
+
+ /**
+ * Returns a new list with only {@link ScheduledRecording} with a {@link
+ * ScheduledRecording#getEndTimeMs() endTime} after now.
+ */
+ private List<ScheduledRecording> filterEndTimeIsPast(List<ScheduledRecording> originals) {
+ List<ScheduledRecording> results = new ArrayList<>(originals.size());
+ for (ScheduledRecording r : originals) {
+ if (r.getEndTimeMs() > mClock.currentTimeMillis()) {
+ results.add(r);
+ }
+ }
+ return results;
+ }
+
+ @Override
+ public List<ScheduledRecording> getStartedRecordings() {
+ return filterEndTimeIsPast(
+ getRecordingsWithState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS));
+ }
+
+ @Override
+ public List<ScheduledRecording> getNonStartedScheduledRecordings() {
+ return filterEndTimeIsPast(
+ getRecordingsWithState(ScheduledRecording.STATE_RECORDING_NOT_STARTED));
+ }
+
+ protected abstract List<ScheduledRecording> getRecordingsWithState(int state);
}
diff --git a/src/com/android/tv/dvr/DvrDataManager.java b/src/com/android/tv/dvr/DvrDataManager.java
index 4f8b0525..c96104e5 100644
--- a/src/com/android/tv/dvr/DvrDataManager.java
+++ b/src/com/android/tv/dvr/DvrDataManager.java
@@ -20,6 +20,8 @@ import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.util.Range;
+import com.android.tv.common.recording.RecordedProgram;
+
import java.util.List;
/**
@@ -32,24 +34,24 @@ public interface DvrDataManager {
boolean isInitialized();
/**
- * Returns recordings.
+ * Returns past recordings.
*/
- List<Recording> getRecordings();
+ List<RecordedProgram> getRecordedPrograms();
/**
- * Returns past recordings.
+ * Returns all {@link ScheduledRecording} regardless of state.
*/
- List<Recording> getFinishedRecordings();
+ List<ScheduledRecording> getAllScheduledRecordings();
/**
- * Returns started recordings.
+ * Returns started recordings that expired.
*/
- List<Recording> getStartedRecordings();
+ List<ScheduledRecording> getStartedRecordings();
/**
- * Returns scheduled recordings
+ * Returns scheduled but not started recordings that have not expired.
*/
- List<Recording> getScheduledRecordings();
+ List<ScheduledRecording> getNonStartedScheduledRecordings();
/**
* Returns season recordings.
@@ -73,27 +75,60 @@ public interface DvrDataManager {
*
* @param period a time period in milliseconds.
*/
- List<Recording> getRecordingsThatOverlapWith(Range<Long> period);
+ List<ScheduledRecording> getRecordingsThatOverlapWith(Range<Long> period);
+
+ /**
+ * Add a {@link ScheduledRecordingListener}.
+ */
+ void addScheduledRecordingListener(ScheduledRecordingListener scheduledRecordingListener);
+
+ /**
+ * Remove a {@link ScheduledRecordingListener}.
+ */
+ void removeScheduledRecordingListener(ScheduledRecordingListener scheduledRecordingListener);
+
+ /**
+ * Add a {@link RecordedProgramListener}.
+ */
+ void addRecordedProgramListener(RecordedProgramListener listener);
/**
- * Add a {@link Listener}.
+ * Remove a {@link RecordedProgramListener}.
*/
- void addListener(Listener listener);
+ void removeRecordedProgramListener(RecordedProgramListener listener);
/**
- * Remove a {@link Listener}.
+ * Returns the scheduled recording program with the given recordingId or null if is not found.
*/
- void removeListener(Listener listener);
+ @Nullable
+ ScheduledRecording getScheduledRecording(long recordingId);
+
+
+ /**
+ * Returns the scheduled recording program with the given programId or null if is not found.
+ */
+ @Nullable
+ ScheduledRecording getScheduledRecordingForProgramId(long programId);
/**
- * Returns the recording with the given recordingId or null if is not found
+ * Returns the recorded program with the given recordingId or null if is not found.
*/
@Nullable
- Recording getRecording(long recordingId);
+ RecordedProgram getRecordedProgram(long recordingId);
+
+ interface ScheduledRecordingListener {
+ void onScheduledRecordingAdded(ScheduledRecording scheduledRecording);
+
+ void onScheduledRecordingRemoved(ScheduledRecording scheduledRecording);
+
+ void onScheduledRecordingStatusChanged(ScheduledRecording scheduledRecording);
+ }
+
+ interface RecordedProgramListener {
+ void onRecordedProgramAdded(RecordedProgram recordedProgram);
+
+ void onRecordedProgramChanged(RecordedProgram recordedProgram);
- interface Listener {
- void onRecordingAdded(Recording recording);
- void onRecordingRemoved(Recording recording);
- void onRecordingStatusChanged(Recording recording);
+ void onRecordedProgramRemoved(RecordedProgram recordedProgram);
}
}
diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java
index 647d9bd7..02c47750 100644
--- a/src/com/android/tv/dvr/DvrDataManagerImpl.java
+++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java
@@ -16,95 +16,174 @@
package com.android.tv.dvr;
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentUris;
import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
+import android.util.ArraySet;
import android.util.Log;
import android.util.Range;
-import com.android.tv.dvr.Recording.RecordingState;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.recording.RecordedProgram;
+import com.android.tv.dvr.ScheduledRecording.RecordingState;
import com.android.tv.dvr.provider.AsyncDvrDbTask;
import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryTask;
-import com.android.tv.util.SoftPreconditions;
+import com.android.tv.util.AsyncDbTask;
+import com.android.tv.util.Clock;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
+import java.util.Set;
/**
* DVR Data manager to handle recordings and schedules.
*/
@MainThread
+@TargetApi(Build.VERSION_CODES.N)
public class DvrDataManagerImpl extends BaseDvrDataManager {
private static final String TAG = "DvrDataManagerImpl";
+ private static final boolean DEBUG = false;
- private Context mContext;
- private boolean mLoadFinished;
- private final HashMap<Long, Recording> mRecordings = new HashMap<>();
- private AsyncDvrQueryTask mQueryTask;
+ private final HashMap<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>();
+ private final HashMap<Long, ScheduledRecording> mProgramId2ScheduledRecordings =
+ new HashMap<>();
+ private final HashMap<Long, RecordedProgram> mRecordedPrograms = new HashMap<>();
- public DvrDataManagerImpl(Context context) {
- super(context);
+ private final Context mContext;
+ private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+ private final ContentObserver mContentObserver = new ContentObserver(mMainThreadHandler) {
+
+ @Override
+ public void onChange(boolean selfChange) {
+ onChange(selfChange, null);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, @Nullable final Uri uri) {
+ if (uri == null) {
+ // TODO reload everything.
+ }
+ AsyncRecordedProgramQueryTask task = new AsyncRecordedProgramQueryTask(
+ mContext.getContentResolver(), uri);
+ task.executeOnDbThread();
+ mPendingTasks.add(task);
+ }
+ };
+
+ private void onObservedChange(Uri uri, RecordedProgram recordedProgram) {
+ long id = ContentUris.parseId(uri);
+ if (DEBUG) {
+ Log.d(TAG, "changed recorded program #" + id + " to " + recordedProgram);
+ }
+ if (recordedProgram == null) {
+ RecordedProgram old = mRecordedPrograms.remove(id);
+ if (old != null) {
+ notifyRecordedProgramRemoved(old);
+ } else {
+ Log.w(TAG, "Could not find old version of deleted program #" + id);
+ }
+ } else {
+ RecordedProgram old = mRecordedPrograms.put(id, recordedProgram);
+ if (old == null) {
+ notifyRecordedProgramAdded(recordedProgram);
+ } else {
+ notifyRecordedProgramChanged(recordedProgram);
+ }
+ }
+ }
+
+ private boolean mDvrLoadFinished;
+ private boolean mRecordedProgramLoadFinished;
+ private final Set<AsyncTask> mPendingTasks = new ArraySet<>();
+
+ public DvrDataManagerImpl(Context context, Clock clock) {
+ super(context, clock);
mContext = context;
}
public void start() {
- mQueryTask = new AsyncDvrQueryTask(mContext) {
+ AsyncDvrQueryTask mDvrQueryTask = new AsyncDvrQueryTask(mContext) {
+
@Override
- protected void onPostExecute(List<Recording> result) {
- mQueryTask = null;
- mLoadFinished = true;
- for (Recording r : result) {
- mRecordings.put(r.getId(), r);
+ protected void onCancelled(List<ScheduledRecording> scheduledRecordings) {
+ mPendingTasks.remove(this);
+ }
+
+ @Override
+ protected void onPostExecute(List<ScheduledRecording> result) {
+ mPendingTasks.remove(this);
+ mDvrLoadFinished = true;
+ for (ScheduledRecording r : result) {
+ mScheduledRecordings.put(r.getId(), r);
}
}
};
- mQueryTask.executeOnDbThread();
+ mDvrQueryTask.executeOnDbThread();
+ mPendingTasks.add(mDvrQueryTask);
+ AsyncRecordedProgramsQueryTask mRecordedProgramQueryTask =
+ new AsyncRecordedProgramsQueryTask(mContext.getContentResolver());
+ mRecordedProgramQueryTask.executeOnDbThread();
+ ContentResolver cr = mContext.getContentResolver();
+ cr.registerContentObserver(TvContract.RecordedPrograms.CONTENT_URI, true, mContentObserver);
}
public void stop() {
- if (mQueryTask != null) {
- mQueryTask.cancel(true);
- mQueryTask = null;
+ ContentResolver cr = mContext.getContentResolver();
+ cr.unregisterContentObserver(mContentObserver);
+ Iterator<AsyncTask> i = mPendingTasks.iterator();
+ while (i.hasNext()) {
+ AsyncTask task = i.next();
+ i.remove();
+ task.cancel(true);
}
}
@Override
public boolean isInitialized() {
- return mLoadFinished;
+ return mDvrLoadFinished && mRecordedProgramLoadFinished;
}
- @Override
- public List<Recording> getRecordings() {
- if (!mLoadFinished) {
+ private List<ScheduledRecording> getScheduledRecordingsPrograms() {
+ if (!mDvrLoadFinished) {
return Collections.emptyList();
}
- ArrayList<Recording> list = new ArrayList<>(mRecordings.size());
- list.addAll(mRecordings.values());
- Collections.sort(list, Recording.START_TIME_COMPARATOR);
- return Collections.unmodifiableList(list);
- }
-
- @Override
- public List<Recording> getFinishedRecordings() {
- return getRecordingsWithState(Recording.STATE_RECORDING_FINISHED);
+ ArrayList<ScheduledRecording> list = new ArrayList<>(mScheduledRecordings.size());
+ list.addAll(mScheduledRecordings.values());
+ Collections.sort(list, ScheduledRecording.START_TIME_COMPARATOR);
+ return list;
}
@Override
- public List<Recording> getStartedRecordings() {
- return getRecordingsWithState(Recording.STATE_RECORDING_IN_PROGRESS);
+ public List<RecordedProgram> getRecordedPrograms() {
+ if (!mRecordedProgramLoadFinished) {
+ return Collections.emptyList();
+ }
+ return new ArrayList<>(mRecordedPrograms.values());
}
@Override
- public List<Recording> getScheduledRecordings() {
- return getRecordingsWithState(Recording.STATE_RECORDING_NOT_STARTED);
+ public List<ScheduledRecording> getAllScheduledRecordings() {
+ return new ArrayList<>(mScheduledRecordings.values());
}
- private List<Recording> getRecordingsWithState(@RecordingState int state) {
- List<Recording> result = new ArrayList<>();
- for (Recording r : mRecordings.values()) {
+ protected List<ScheduledRecording> getRecordingsWithState(@RecordingState int state) {
+ List<ScheduledRecording> result = new ArrayList<>();
+ for (ScheduledRecording r : mScheduledRecordings.values()) {
if (r.getState() == state) {
result.add(r);
}
@@ -120,29 +199,29 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
@Override
public long getNextScheduledStartTimeAfter(long startTime) {
- return getNextStartTimeAfter(getRecordings(), startTime);
+ return getNextStartTimeAfter(getScheduledRecordingsPrograms(), startTime);
}
@VisibleForTesting
- static long getNextStartTimeAfter(List<Recording> recordings, long startTime) {
+ static long getNextStartTimeAfter(List<ScheduledRecording> scheduledRecordings, long startTime) {
int start = 0;
- int end = recordings.size() - 1;
+ int end = scheduledRecordings.size() - 1;
while (start <= end) {
int mid = (start + end) / 2;
- if (recordings.get(mid).getStartTimeMs() <= startTime) {
+ if (scheduledRecordings.get(mid).getStartTimeMs() <= startTime) {
start = mid + 1;
} else {
end = mid - 1;
}
}
- return start < recordings.size() ? recordings.get(start).getStartTimeMs()
+ return start < scheduledRecordings.size() ? scheduledRecordings.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.values()) {
+ public List<ScheduledRecording> getRecordingsThatOverlapWith(Range<Long> period) {
+ List<ScheduledRecording> result = new ArrayList<>();
+ for (ScheduledRecording r : mScheduledRecordings.values()) {
if (r.isOverLapping(period)) {
result.add(r);
}
@@ -152,38 +231,56 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
@Nullable
@Override
- public Recording getRecording(long recordingId) {
- if (mLoadFinished) {
- return mRecordings.get(recordingId);
+ public ScheduledRecording getScheduledRecording(long recordingId) {
+ if (mDvrLoadFinished) {
+ return mScheduledRecordings.get(recordingId);
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public ScheduledRecording getScheduledRecordingForProgramId(long programId) {
+ if (mDvrLoadFinished) {
+ return mProgramId2ScheduledRecordings.get(programId);
}
return null;
}
+ @Nullable
+ @Override
+ public RecordedProgram getRecordedProgram(long recordingId) {
+ return mRecordedPrograms.get(recordingId);
+ }
+
@Override
- public void addRecording(final Recording recording) {
+ public void addScheduledRecording(final ScheduledRecording scheduledRecording) {
new AsyncDvrDbTask.AsyncAddRecordingTask(mContext) {
@Override
- protected void onPostExecute(List<Recording> recordings) {
- super.onPostExecute(recordings);
- SoftPreconditions.checkArgument(recordings.size() == 1);
- for (Recording r : recordings) {
+ protected void onPostExecute(List<ScheduledRecording> scheduledRecordings) {
+ super.onPostExecute(scheduledRecordings);
+ SoftPreconditions.checkArgument(scheduledRecordings.size() == 1);
+ for (ScheduledRecording r : scheduledRecordings) {
if (r.getId() != -1) {
- mRecordings.put(r.getId(), r);
- notifyRecordingAdded(r);
+ mScheduledRecordings.put(r.getId(), r);
+ if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
+ mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
+ }
+ notifyScheduledRecordingAdded(r);
} else {
Log.w(TAG, "Error adding " + r);
}
}
}
- }.executeOnDbThread(recording);
+ }.executeOnDbThread(scheduledRecording);
}
@Override
public void addSeasonRecording(SeasonRecording seasonRecording) { }
@Override
- public void removeRecording(final Recording recording) {
+ public void removeScheduledRecording(final ScheduledRecording scheduledRecording) {
new AsyncDvrDbTask.AsyncDeleteRecordingTask(mContext) {
@Override
protected void onPostExecute(List<Integer> counts) {
@@ -191,23 +288,27 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
SoftPreconditions.checkArgument(counts.size() == 1);
for (Integer c : counts) {
if (c == 1) {
- mRecordings.remove(recording.getId());
+ mScheduledRecordings.remove(scheduledRecording.getId());
+ if (scheduledRecording.getProgramId() != ScheduledRecording.ID_NOT_SET) {
+ mProgramId2ScheduledRecordings
+ .remove(scheduledRecording.getProgramId());
+ }
//TODO change to notifyRecordingUpdated
- notifyRecordingRemoved(recording);
+ notifyScheduledRecordingRemoved(scheduledRecording);
} else {
- Log.w(TAG, "Error removing " + recording);
+ Log.w(TAG, "Error removing " + scheduledRecording);
}
}
}
- }.executeOnDbThread(recording);
+ }.executeOnDbThread(scheduledRecording);
}
@Override
public void removeSeasonSchedule(SeasonRecording seasonSchedule) { }
@Override
- public void updateRecording(final Recording recording) {
+ public void updateScheduledRecording(final ScheduledRecording scheduledRecording) {
new AsyncDvrDbTask.AsyncUpdateRecordingTask(mContext) {
@Override
protected void onPostExecute(List<Integer> counts) {
@@ -215,15 +316,88 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
SoftPreconditions.checkArgument(counts.size() == 1);
for (Integer c : counts) {
if (c == 1) {
- mRecordings.put(recording.getId(), recording);
+ ScheduledRecording oldScheduledRecording = mScheduledRecordings
+ .put(scheduledRecording.getId(), scheduledRecording);
+ long programId = scheduledRecording.getProgramId();
+ if (oldScheduledRecording != null
+ && oldScheduledRecording.getProgramId() != programId
+ && oldScheduledRecording.getProgramId()
+ != ScheduledRecording.ID_NOT_SET) {
+ ScheduledRecording oldValueForProgramId = mProgramId2ScheduledRecordings
+ .get(oldScheduledRecording.getProgramId());
+ if (oldValueForProgramId.getId() == scheduledRecording.getId()) {
+ //Only remove the old ScheduledRecording if it has the same ID as
+ // the new one.
+ mProgramId2ScheduledRecordings
+ .remove(oldScheduledRecording.getProgramId());
+ }
+ }
+ if (programId != ScheduledRecording.ID_NOT_SET) {
+ mProgramId2ScheduledRecordings.put(programId, scheduledRecording);
+ }
//TODO change to notifyRecordingUpdated
- notifyRecordingStatusChanged(recording);
+ notifyScheduledRecordingStatusChanged(scheduledRecording);
} else {
- Log.w(TAG, "Error updating " + recording);
+ Log.w(TAG, "Error updating " + scheduledRecording);
}
}
+ }
+ }.executeOnDbThread(scheduledRecording);
+ }
+
+ private final class AsyncRecordedProgramsQueryTask
+ extends AsyncDbTask.AsyncQueryListTask<RecordedProgram> {
+ public AsyncRecordedProgramsQueryTask(ContentResolver contentResolver) {
+ super(contentResolver, TvContract.RecordedPrograms.CONTENT_URI,
+ RecordedProgram.PROJECTION, null, null, null);
+ }
+
+ @Override
+ protected RecordedProgram fromCursor(Cursor c) {
+ return RecordedProgram.fromCursor(c);
+ }
+
+ @Override
+ protected void onCancelled(List<RecordedProgram> scheduledRecordings) {
+ mPendingTasks.remove(this);
+ }
+ @Override
+ protected void onPostExecute(List<RecordedProgram> result) {
+ mPendingTasks.remove(this);
+ mRecordedProgramLoadFinished = true;
+ if (result != null) {
+ for (RecordedProgram r : result) {
+ mRecordedPrograms.put(r.getId(), r);
+ }
}
- }.executeOnDbThread(recording);
+ }
+ }
+
+ private final class AsyncRecordedProgramQueryTask
+ extends AsyncDbTask.AsyncQueryItemTask<RecordedProgram> {
+
+ private final Uri mUri;
+
+ public AsyncRecordedProgramQueryTask(ContentResolver contentResolver, Uri uri) {
+ super(contentResolver, uri, RecordedProgram.PROJECTION, null, null, null);
+ mUri = uri;
+ }
+
+ @Override
+ protected RecordedProgram fromCursor(Cursor c) {
+ return RecordedProgram.fromCursor(c);
+ }
+
+ @Override
+ protected void onCancelled(RecordedProgram recordedProgram) {
+ mPendingTasks.remove(this);
+ }
+
+ @Override
+ protected void onPostExecute(RecordedProgram recordedProgram) {
+ mPendingTasks.remove(this);
+ onObservedChange(mUri, recordedProgram);
+ }
}
}
diff --git a/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java b/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java
index 8a19cb29..95b342bb 100644
--- a/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java
+++ b/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java
@@ -23,7 +23,9 @@ import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.Range;
-import com.android.tv.util.SoftPreconditions;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.recording.RecordedProgram;
+import com.android.tv.util.Clock;
import java.util.ArrayList;
import java.util.Collections;
@@ -40,11 +42,12 @@ import java.util.concurrent.atomic.AtomicLong;
public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager {
private final static String TAG = "DvrDataManagerInMemory";
private final AtomicLong mNextId = new AtomicLong(1);
- private final Map<Long, Recording> mRecordings = new HashMap<>();
- private List<SeasonRecording> mSeasonSchedule = new ArrayList<>();
+ private final Map<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>();
+ private final Map<Long, RecordedProgram> mRecordedPrograms = new HashMap<>();
+ private final List<SeasonRecording> mSeasonSchedule = new ArrayList<>();
- public DvrDataManagerInMemoryImpl(Context context) {
- super(context);
+ public DvrDataManagerInMemoryImpl(Context context, Clock clock) {
+ super(context, clock);
}
@Override
@@ -52,27 +55,20 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager {
return true;
}
- @Override
- public List<Recording> getRecordings() {
- return new ArrayList(mRecordings.values());
- }
-
- @Override
- public List<Recording> getFinishedRecordings() {
- return getRecordingsWithState(Recording.STATE_RECORDING_FINISHED);
+ private List<ScheduledRecording> getScheduledRecordingsPrograms() {
+ return new ArrayList(mScheduledRecordings.values());
}
@Override
- public List<Recording> getStartedRecordings() {
- return getRecordingsWithState(Recording.STATE_RECORDING_IN_PROGRESS);
+ public List<RecordedProgram> getRecordedPrograms() {
+ return new ArrayList<>(mRecordedPrograms.values());
}
@Override
- public List<Recording> getScheduledRecordings() {
- return getRecordingsWithState(Recording.STATE_RECORDING_NOT_STARTED);
+ public List<ScheduledRecording> getAllScheduledRecordings() {
+ return new ArrayList<>(mScheduledRecordings.values());
}
- @Override
public List<SeasonRecording> getSeasonRecordings() {
return mSeasonSchedule;
}
@@ -80,9 +76,9 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager {
@Override
public long getNextScheduledStartTimeAfter(long startTime) {
- List<Recording> temp = getScheduledRecordings();
- Collections.sort(temp, Recording.START_TIME_COMPARATOR);
- for (Recording r : temp) {
+ List<ScheduledRecording> temp = getNonStartedScheduledRecordings();
+ Collections.sort(temp, ScheduledRecording.START_TIME_COMPARATOR);
+ for (ScheduledRecording r : temp) {
if (r.getStartTimeMs() > startTime) {
return r.getStartTimeMs();
}
@@ -91,10 +87,10 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager {
}
@Override
- public List<Recording> getRecordingsThatOverlapWith(Range<Long> period) {
- List<Recording> temp = getRecordings();
- List<Recording> result = new ArrayList<>();
- for (Recording r : temp) {
+ public List<ScheduledRecording> getRecordingsThatOverlapWith(Range<Long> period) {
+ List<ScheduledRecording> temp = getScheduledRecordingsPrograms();
+ List<ScheduledRecording> result = new ArrayList<>();
+ for (ScheduledRecording r : temp) {
if (r.isOverLapping(period)) {
result.add(r);
}
@@ -103,20 +99,56 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager {
}
/**
- * Add a new recording.
+ * Add a new scheduled recording.
*/
@Override
- public void addRecording(Recording recording) {
- addRecordingInternal(recording);
+ public void addScheduledRecording(ScheduledRecording scheduledRecording) {
+ addScheduledRecordingInternal(scheduledRecording);
+ }
+
+
+ public void addRecordedProgram(RecordedProgram recordedProgram) {
+ addRecordedProgramInternal(recordedProgram);
+ }
+
+ public void updateRecordedProgram(RecordedProgram r) {
+ long id = r.getId();
+ if (mRecordedPrograms.containsKey(id)) {
+ mRecordedPrograms.put(id, r);
+ notifyRecordedProgramChanged(r);
+ } else {
+ throw new IllegalArgumentException("Recording not found:" + r);
+ }
+ }
+
+ public void removeRecordedProgram(RecordedProgram scheduledRecording) {
+ mRecordedPrograms.remove(scheduledRecording.getId());
+ notifyRecordedProgramRemoved(scheduledRecording);
+ }
+
+
+ public ScheduledRecording addScheduledRecordingInternal(ScheduledRecording scheduledRecording) {
+ SoftPreconditions
+ .checkState(scheduledRecording.getId() == ScheduledRecording.ID_NOT_SET, TAG,
+ "expected id of " + ScheduledRecording.ID_NOT_SET + " but was "
+ + scheduledRecording);
+ scheduledRecording = ScheduledRecording.buildFrom(scheduledRecording)
+ .setId(mNextId.incrementAndGet())
+ .build();
+ mScheduledRecordings.put(scheduledRecording.getId(), scheduledRecording);
+ notifyScheduledRecordingAdded(scheduledRecording);
+ return scheduledRecording;
}
- public Recording addRecordingInternal(Recording recording) {
- SoftPreconditions.checkState(recording.getId() == Recording.ID_NOT_SET, TAG,
- "expected id of " + Recording.ID_NOT_SET + " but was " + recording);
- recording = Recording.buildFrom(recording).setId(mNextId.incrementAndGet()).build();
- mRecordings.put(recording.getId(), recording);
- notifyRecordingAdded(recording);
- return recording;
+ public RecordedProgram addRecordedProgramInternal(RecordedProgram recordedProgram) {
+ SoftPreconditions.checkState(recordedProgram.getId() == RecordedProgram.ID_NOT_SET, TAG,
+ "expected id of " + RecordedProgram.ID_NOT_SET + " but was " + recordedProgram);
+ recordedProgram = RecordedProgram.buildFrom(recordedProgram)
+ .setId(mNextId.incrementAndGet())
+ .build();
+ mRecordedPrograms.put(recordedProgram.getId(), recordedProgram);
+ notifyRecordedProgramAdded(recordedProgram);
+ return recordedProgram;
}
@Override
@@ -125,9 +157,9 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager {
}
@Override
- public void removeRecording(Recording recording) {
- mRecordings.remove(recording.getId());
- notifyRecordingRemoved(recording);
+ public void removeScheduledRecording(ScheduledRecording scheduledRecording) {
+ mScheduledRecordings.remove(scheduledRecording.getId());
+ notifyScheduledRecordingRemoved(scheduledRecording);
}
@Override
@@ -136,11 +168,11 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager {
}
@Override
- public void updateRecording(Recording r) {
+ public void updateScheduledRecording(ScheduledRecording r) {
long id = r.getId();
- if (mRecordings.containsKey(id)) {
- mRecordings.put(id, r);
- notifyRecordingStatusChanged(r);
+ if (mScheduledRecordings.containsKey(id)) {
+ mScheduledRecordings.put(id, r);
+ notifyScheduledRecordingStatusChanged(r);
} else {
throw new IllegalArgumentException("Recording not found:" + r);
}
@@ -148,14 +180,32 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager {
@Nullable
@Override
- public Recording getRecording(long id) {
- return mRecordings.get(id);
+ public ScheduledRecording getScheduledRecording(long id) {
+ return mScheduledRecordings.get(id);
}
+ @Nullable
+ @Override
+ public ScheduledRecording getScheduledRecordingForProgramId(long programId) {
+ for (ScheduledRecording r : mScheduledRecordings.values()) {
+ if (r.getProgramId() == programId) {
+ return r;
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public RecordedProgram getRecordedProgram(long recordingId) {
+ return mRecordedPrograms.get(recordingId);
+ }
+
+ @Override
@NonNull
- private List<Recording> getRecordingsWithState(int state) {
- ArrayList<Recording> result = new ArrayList<>();
- for (Recording r : mRecordings.values()) {
+ protected List<ScheduledRecording> getRecordingsWithState(int state) {
+ ArrayList<ScheduledRecording> result = new ArrayList<>();
+ for (ScheduledRecording r : mScheduledRecordings.values()) {
if(r.getState() == state){
result.add(r);
}
diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java
index c62c564b..e3dc622e 100644
--- a/src/com/android/tv/dvr/DvrManager.java
+++ b/src/com/android/tv/dvr/DvrManager.java
@@ -16,24 +16,33 @@
package com.android.tv.dvr;
+import android.content.ContentResolver;
import android.content.Context;
+import android.media.tv.TvInputInfo;
+import android.os.Handler;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
import android.util.Log;
import android.util.Range;
+import android.widget.Toast;
import com.android.tv.ApplicationSingletons;
import com.android.tv.TvApplication;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.recording.RecordingCapability;
+import com.android.tv.common.recording.RecordedProgram;
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.AsyncDbTask;
import com.android.tv.util.Utils;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
/**
* DVR manager class to add and remove recordings. UI can modify recording list through this class,
@@ -45,11 +54,15 @@ public class DvrManager {
private final WritableDvrDataManager mDataManager;
private final ChannelDataManager mChannelDataManager;
private final DvrSessionManager mDvrSessionManager;
+ // @GuardedBy("mListener")
+ private final Map<Listener, Handler> mListener = new HashMap<>();
+ private final Context mAppContext;
public DvrManager(Context context) {
SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG);
ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager();
+ mAppContext = context.getApplicationContext();
mChannelDataManager = appSingletons.getChannelDataManager();
mDvrSessionManager = appSingletons.getDvrSessionManger();
}
@@ -59,17 +72,18 @@ public class DvrManager {
* @param program the program to record
* @param recordingsToOverride the possible empty list of recordings that will not be recorded
*/
- public void addSchedule(Program program, List<Recording> recordingsToOverride) {
+ public void addSchedule(Program program, List<ScheduledRecording> recordingsToOverride) {
Log.i(TAG,
"Adding scheduled recording of " + program + " instead of " + recordingsToOverride);
- Collections.sort(recordingsToOverride, Recording.PRIORITY_COMPARATOR);
+ Collections.sort(recordingsToOverride, ScheduledRecording.PRIORITY_COMPARATOR);
Channel c = mChannelDataManager.getChannel(program.getChannelId());
long priority = recordingsToOverride.isEmpty() ? Long.MAX_VALUE
: recordingsToOverride.get(0).getPriority() - 1;
- Recording r = Recording.builder(c, program)
+ ScheduledRecording r = ScheduledRecording.builder(program)
.setPriority(priority)
+ .setChannelId(c.getId())
.build();
- mDataManager.addRecording(r);
+ mDataManager.addScheduledRecording(r);
}
/**
@@ -79,8 +93,10 @@ public class DvrManager {
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);
+ ScheduledRecording r = ScheduledRecording.builder(startTime, endTime)
+ .setChannelId(channel.getId())
+ .build();
+ mDataManager.addScheduledRecording(r);
}
/**
@@ -92,12 +108,45 @@ public class DvrManager {
}
/**
+ * Stops the currently recorded program
+ */
+ public void stopRecording(final ScheduledRecording recording) {
+ synchronized (mListener) {
+ for (final Entry<Listener, Handler> entry : mListener.entrySet()) {
+ entry.getValue().post(new Runnable() {
+ @Override
+ public void run() {
+ entry.getKey().onStopRecordingRequested(recording);
+ }
+ });
+ }
+ }
+ }
+
+ /**
* Removes a scheduled recording or an existing recording.
*/
- public void removeRecording(Recording recording) {
- Log.i(TAG, "Removing " + recording);
- // TODO(DVR): ask the TIS to delete the recording and respond to the result.
- mDataManager.removeRecording(recording);
+ public void removeScheduledRecording(ScheduledRecording scheduledRecording) {
+ Log.i(TAG, "Removing " + scheduledRecording);
+ mDataManager.removeScheduledRecording(scheduledRecording);
+ }
+
+ public void removeRecordedProgram(final RecordedProgram recordedProgram) {
+ // TODO(dvr): implement
+ Log.i(TAG, "To delete " + recordedProgram
+ + "\nyou should manually delete video data at"
+ + "\nadb shell rm -rf " + recordedProgram.getDataUri()
+ );
+ Toast.makeText(mAppContext, "Deleting recorded programs is not fully implemented yet",
+ Toast.LENGTH_SHORT).show();
+ new AsyncDbTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ ContentResolver resolver = mAppContext.getContentResolver();
+ resolver.delete(recordedProgram.getUri(), null, null);
+ return null;
+ }
+ }.execute();
}
/**
@@ -107,17 +156,21 @@ public class DvrManager {
* <p>Any empty list means there is no conflicts. If there is conflict the program must be
* scheduled to record with a Priority lower than the first Recording in the list returned.
*/
- public List<Recording> getScheduledRecordingsThatConflict(Program program) {
+ public List<ScheduledRecording> getScheduledRecordingsThatConflict(Program program) {
//TODO(DVR): move to scheduler.
//TODO(DVR): deal with more than one DvrInputService
- List<Recording> overLap = mDataManager.getRecordingsThatOverlapWith(getPeriod(program));
+ List<ScheduledRecording> overLap = mDataManager.getRecordingsThatOverlapWith(getPeriod(program));
if (!overLap.isEmpty()) {
// TODO(DVR): ignore shows that already won't record.
Channel channel = mChannelDataManager.getChannel(program.getChannelId());
if (channel != null) {
- RecordingCapability recordingCapability = mDvrSessionManager
- .getRecordingCapability(channel.getInputId());
- int remove = Math.max(0, recordingCapability.maxConcurrentTunedSessions - 1);
+ TvInputInfo info = mDvrSessionManager.getTvInputInfo(channel.getInputId());
+ if (info == null) {
+ Log.w(TAG,
+ "Could not find a recording TvInputInfo for " + channel.getInputId());
+ return overLap;
+ }
+ int remove = Math.max(0, info.getTunerCount() - 1);
if (remove >= overLap.size()) {
return Collections.EMPTY_LIST;
}
@@ -136,7 +189,7 @@ public class DvrManager {
* 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) {
+ public boolean canTuneTo(Channel channel, List<ScheduledRecording> outConflictScheduledRecordings) {
// TODO: implement
return true;
}
@@ -145,8 +198,29 @@ public class DvrManager {
* Returns true is the inputId supports recording.
*/
public boolean canRecord(String inputId) {
- RecordingCapability recordingCapability = mDvrSessionManager
- .getRecordingCapability(inputId);
- return recordingCapability != null && recordingCapability.maxConcurrentTunedSessions > 0;
+ TvInputInfo info = mDvrSessionManager.getTvInputInfo(inputId);
+ return info != null && info.getTunerCount() > 0;
+ }
+
+ @WorkerThread
+ void addListener(Listener listener, @NonNull Handler handler) {
+ SoftPreconditions.checkNotNull(handler);
+ synchronized (mListener) {
+ mListener.put(listener, handler);
+ }
+ }
+
+ @WorkerThread
+ void removeListener(Listener listener) {
+ synchronized (mListener) {
+ mListener.remove(listener);
+ }
+ }
+
+ /**
+ * Listener internally used inside dvr package.
+ */
+ interface Listener {
+ void onStopRecordingRequested(ScheduledRecording scheduledRecording);
}
}
diff --git a/src/com/android/tv/dvr/DvrPlayActivity.java b/src/com/android/tv/dvr/DvrPlayActivity.java
index 872e05bd..b117a7cf 100644
--- a/src/com/android/tv/dvr/DvrPlayActivity.java
+++ b/src/com/android/tv/dvr/DvrPlayActivity.java
@@ -24,7 +24,7 @@ import com.android.tv.R;
import com.android.tv.TvApplication;
/**
- * Simple Activity to play a {@link Recording}.
+ * Simple Activity to play a {@link ScheduledRecording}.
*/
public class DvrPlayActivity extends Activity {
@@ -35,11 +35,11 @@ public class DvrPlayActivity extends Activity {
DvrDataManager dvrDataManager = TvApplication.getSingletons(this).getDvrDataManager();
// TODO(DVR) handle errors.
- long recordingId = getIntent().getLongExtra(Recording.RECORDING_ID_EXTRA, 0);
- Recording recording = dvrDataManager.getRecording(recordingId);
+ long recordingId = getIntent().getLongExtra(ScheduledRecording.RECORDING_ID_EXTRA, 0);
+ ScheduledRecording scheduledRecording = dvrDataManager.getScheduledRecording(recordingId);
TextView textView = (TextView) findViewById(R.id.placeHolderText);
- if (recording != null) {
- textView.setText(recording.toString());
+ if (scheduledRecording != null) {
+ textView.setText(scheduledRecording.toString());
} else {
textView.setText(R.string.ut_result_not_found_title); // TODO(DVR) update error text
}
diff --git a/src/com/android/tv/dvr/DvrRecordingService.java b/src/com/android/tv/dvr/DvrRecordingService.java
index d0e86d50..2f3abccf 100644
--- a/src/com/android/tv/dvr/DvrRecordingService.java
+++ b/src/com/android/tv/dvr/DvrRecordingService.java
@@ -31,7 +31,8 @@ import com.android.tv.ApplicationSingletons;
import com.android.tv.TvApplication;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.util.Clock;
-import com.android.tv.util.SoftPreconditions;
+import com.android.tv.util.RecurringRunner;
+import com.android.tv.common.SoftPreconditions;
/**
* DVR Scheduler service.
@@ -57,6 +58,8 @@ public class DvrRecordingService extends Service {
context.startService(dvrSchedulerIntent);
}
+ private final Clock mClock = Clock.SYSTEM;
+ private RecurringRunner mReaperRunner;
private WritableDvrDataManager mDataManager;
/**
@@ -86,14 +89,16 @@ public class DvrRecordingService extends Service {
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
// mScheduler may have been set for testing.
if (mScheduler == null) {
- DvrSessionManager sessionManager = singletons.getDvrSessionManger();
mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME);
mHandlerThread.start();
- mScheduler = new Scheduler(mHandlerThread.getLooper(), sessionManager, mDataManager,
- this, Clock.SYSTEM,
- alarmManager);
+ mScheduler = new Scheduler(mHandlerThread.getLooper(), singletons.getDvrManager(),
+ singletons.getDvrSessionManger(), mDataManager,
+ singletons.getChannelDataManager(), this, mClock, alarmManager);
}
- mDataManager.addListener(mScheduler);
+ mDataManager.addScheduledRecordingListener(mScheduler);
+ mReaperRunner = new RecurringRunner(this, java.util.concurrent.TimeUnit.DAYS.toMillis(1),
+ new ScheduledProgramReaper(mDataManager, mClock), null);
+ mReaperRunner.start();
}
@Override
@@ -106,7 +111,8 @@ public class DvrRecordingService extends Service {
@Override
public void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy");
- mDataManager.removeListener(mScheduler);
+ mReaperRunner.stop();
+ mDataManager.removeScheduledRecordingListener(mScheduler);
mScheduler = null;
if (mHandlerThread != null) {
mHandlerThread.quit();
diff --git a/src/com/android/tv/dvr/DvrSessionManager.java b/src/com/android/tv/dvr/DvrSessionManager.java
index 553001e2..fba05cb6 100644
--- a/src/com/android/tv/dvr/DvrSessionManager.java
+++ b/src/com/android/tv/dvr/DvrSessionManager.java
@@ -16,18 +16,21 @@
package com.android.tv.dvr;
-import android.content.ComponentName;
+import android.annotation.TargetApi;
import android.content.Context;
-import android.media.tv.TvContract;
+import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvRecordingClient;
+import android.os.Build;
+import android.os.Handler;
import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
import android.support.v4.util.ArrayMap;
+import android.util.Log;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.recording.RecordingCapability;
-import com.android.tv.common.recording.TvRecording;
import com.android.tv.data.Channel;
-import com.android.tv.util.SoftPreconditions;
-import com.android.usbtuner.tvinput.UsbTunerTvInputService;
/**
* Manages Dvr Sessions.
@@ -37,57 +40,91 @@ import com.android.usbtuner.tvinput.UsbTunerTvInputService;
* <li>Manage capabilities (conflict)</li>
* </ul>
*/
-public class DvrSessionManager {
+@TargetApi(Build.VERSION_CODES.N)
+public class DvrSessionManager extends TvInputManager.TvInputCallback {
+ //consider moving all of this to TvInputManagerHelper
private final static String TAG = "DvrSessionManager";
+ private static final boolean DEBUG = false;
+
private final Context mContext;
- private TvRecording.TvRecordingClient mRecordingClient;
- private ArrayMap<String, RecordingCapability> mCapabilityMap = new ArrayMap<>();
+ private final TvInputManager mTvInputManager;
+ private final ArrayMap<String, TvInputInfo> mRecordingTvInputs = new ArrayMap<>();
public DvrSessionManager(Context context) {
+ this(context, (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE),
+ new Handler());
+ }
+
+ @VisibleForTesting
+ DvrSessionManager(Context context, TvInputManager tvInputManager, Handler handler) {
SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG);
+ mTvInputManager = tvInputManager;
mContext = context.getApplicationContext();
- // TODO(DVR): get a session to all clients, for now just get USB a TestInput
- final String inputId = TvContract
- .buildInputId(new ComponentName(context, UsbTunerTvInputService.class));
- mRecordingClient = acquireDvrSession(inputId, null);
- mRecordingClient.connect(inputId, new TvRecording.ClientCallback() {
- @Override
- public void onCapabilityReceived(RecordingCapability capability) {
- mCapabilityMap.put(inputId, capability);
- mRecordingClient.release();
- mRecordingClient = null;
+ for (TvInputInfo info : tvInputManager.getTvInputList()) {
+ if (DEBUG) {
+ Log.d(TAG, info + " canRecord=" + info.canRecord() + " tunerCount=" + info
+ .getTunerCount());
+ }
+ if (info.canRecord()) {
+ mRecordingTvInputs.put(info.getId(), info);
}
- });
- if (CommonFeatures.DVR.isEnabled(context)) { // STOPSHIP(DVR)
- String testInputId = "com.android.tv.testinput/.TestTvInputService";
- mCapabilityMap.put(testInputId,
- RecordingCapability.builder()
- .setInputId(testInputId)
- .setMaxConcurrentPlayingSessions(2)
- .setMaxConcurrentTunedSessions(2)
- .setMaxConcurrentSessionsOfAllTypes(3)
- .build());
-
}
+ tvInputManager.registerCallback(this, handler);
+
}
- public TvRecording.TvRecordingClient acquireDvrSession(String inputId, Channel channel) {
- // TODO(DVR): use input and channel or change API
- TvRecording.TvRecordingClient sessionClient = new TvRecording.TvRecordingClient(mContext);
- return sessionClient;
+ public TvRecordingClient createTvRecordingClient(String tag,
+ TvRecordingClient.RecordingCallback callback, Handler handler) {
+ return new TvRecordingClient(mContext, tag, callback, handler);
}
public boolean canAcquireDvrSession(String inputId, Channel channel) {
- // TODO(DVR): implement
- return true;
+ // TODO(DVR): implement checking tuner count etc.
+ TvInputInfo info = mRecordingTvInputs.get(inputId);
+ return info != null;
+ }
+
+ public void releaseTvRecordingClient(TvRecordingClient recordingClient) {
+ recordingClient.release();
}
- public void releaseDvrSession(TvRecording.TvRecordingClient session) {
- session.release();
+ @Override
+ public void onInputAdded(String inputId) {
+ super.onInputAdded(inputId);
+ TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
+ if (DEBUG) {
+ Log.d(TAG, "onInputAdded " + info.toString() + " canRecord=" + info.canRecord()
+ + " tunerCount=" + info.getTunerCount());
+ }
+ if (info.canRecord()) {
+ mRecordingTvInputs.put(inputId, info);
+ }
+ }
+
+ @Override
+ public void onInputRemoved(String inputId) {
+ super.onInputRemoved(inputId);
+ if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId);
+ mRecordingTvInputs.remove(inputId);
+ }
+
+ @Override
+ public void onInputUpdated(String inputId) {
+ super.onInputUpdated(inputId);
+ TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
+ if (DEBUG) {
+ Log.d(TAG, "onInputUpdated " + info.toString() + " canRecord=" + info.canRecord()
+ + " tunerCount=" + info.getTunerCount());
+ }
+ if (info.canRecord()) {
+ mRecordingTvInputs.put(inputId, info);
+ } else {
+ mRecordingTvInputs.remove(inputId);
+ }
}
@Nullable
- public RecordingCapability getRecordingCapability(String inputId) {
- return mCapabilityMap.get(inputId);
+ public TvInputInfo getTvInputInfo(String inputId) {
+ return mRecordingTvInputs.get(inputId);
}
}
diff --git a/src/com/android/tv/dvr/RecordingTask.java b/src/com/android/tv/dvr/RecordingTask.java
index 3bed5e77..804485b3 100644
--- a/src/com/android/tv/dvr/RecordingTask.java
+++ b/src/com/android/tv/dvr/RecordingTask.java
@@ -16,6 +16,8 @@
package com.android.tv.dvr;
+import android.media.tv.TvContract;
+import android.media.tv.TvRecordingClient;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
@@ -24,10 +26,9 @@ import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import android.util.Log;
-import com.android.tv.common.recording.TvRecording;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.util.Clock;
-import com.android.tv.util.SoftPreconditions;
import com.android.tv.util.Utils;
import java.util.concurrent.TimeUnit;
@@ -39,9 +40,10 @@ import java.util.concurrent.TimeUnit;
* There is only one looper so messages must be handled quickly or start a separate thread.
*/
@WorkerThread
-class RecordingTask extends TvRecording.ClientCallback implements Handler.Callback {
+class RecordingTask extends TvRecordingClient.RecordingCallback
+ implements Handler.Callback, DvrManager.Listener {
private static final String TAG = "RecordingTask";
- private static final boolean DEBUG = true; //STOPSHIP(DVR)
+ private static final boolean DEBUG = false;
@VisibleForTesting
static final int MESSAGE_INIT = 1;
@@ -51,11 +53,10 @@ class RecordingTask extends TvRecording.ClientCallback implements Handler.Callba
static final int MESSAGE_STOP_RECORDING = 3;
@VisibleForTesting
- static long MS_BEFORE_START = TimeUnit.SECONDS.toMillis(5);
+ static final long MS_BEFORE_START = TimeUnit.SECONDS.toMillis(5);
@VisibleForTesting
- static long MS_AFTER_END = TimeUnit.SECONDS.toMillis(5);
+ static final long MS_AFTER_END = TimeUnit.SECONDS.toMillis(5);
- //STOPSHIP(DVR) don't use enums.
@VisibleForTesting
enum State {
NOT_STARTED,
@@ -64,27 +65,33 @@ class RecordingTask extends TvRecording.ClientCallback implements Handler.Callba
CONNECTED,
RECORDING_START_REQUESTED,
RECORDING_STARTED,
+ RECORDING_STOP_REQUESTED,
ERROR,
RELEASED,
}
private final DvrSessionManager mSessionManager;
+ private final DvrManager mDvrManager;
private final WritableDvrDataManager mDataManager;
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
- private TvRecording.TvRecordingClient mSession;
+ private TvRecordingClient mTvRecordingClient;
private Handler mHandler;
- private Recording mRecording;
+ private ScheduledRecording mScheduledRecording;
+ private final Channel mChannel;
private State mState = State.NOT_STARTED;
private final Clock mClock;
- RecordingTask(Recording recording, DvrSessionManager sessionManager,
+ RecordingTask(ScheduledRecording scheduledRecording, Channel channel,
+ DvrManager dvrManager, DvrSessionManager sessionManager,
WritableDvrDataManager dataManager, Clock clock) {
- mRecording = recording;
+ mScheduledRecording = scheduledRecording;
+ mChannel = channel;
mSessionManager = sessionManager;
mDataManager = dataManager;
mClock = clock;
+ mDvrManager = dvrManager;
- if (DEBUG) Log.d(TAG, "created recording task " + mRecording);
+ if (DEBUG) Log.d(TAG, "created recording task " + mScheduledRecording);
}
public void setHandler(Handler handler) {
@@ -118,60 +125,45 @@ class RecordingTask extends TvRecording.ClientCallback implements Handler.Callba
}
return true;
} catch (Exception e) {
- Log.w(TAG, "Error processing message " + msg + " for " + mRecording, e);
+ Log.w(TAG, "Error processing message " + msg + " for " + mScheduledRecording, e);
failAndQuit();
}
return false;
}
@Override
- public void onConnected() {
- if (DEBUG) Log.d(TAG, "onConnected");
- super.onConnected();
+ public void onTuned(Uri channelUri) {
+ if (DEBUG) {
+ Log.d(TAG, "onTuned");
+ }
+ super.onTuned(channelUri);
mState = State.CONNECTED;
+ if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_START_RECORDING,
+ mScheduledRecording.getStartTimeMs() - MS_BEFORE_START)) {
+ mState = State.ERROR;
+ return;
+ }
}
- @Override
- public void onDisconnected() {
- if (DEBUG) Log.d(TAG, "onDisconnected");
- super.onDisconnected();
- //Do nothing
- }
-
- @Override
- public void onRecordDeleted(Uri mediaUri) {
- if (DEBUG) Log.d(TAG, "onRecordDeleted " + mediaUri);
- super.onRecordDeleted(mediaUri);
- SoftPreconditions.checkState(false, TAG, "unexpected onRecordDeleted");
-
- }
-
- @Override
- public void onRecordDeleteFailed(Uri mediaUri, int reason) {
- if (DEBUG) Log.d(TAG, "onRecordDeleteFailed " + mediaUri + ", " + reason);
- super.onRecordDeleteFailed(mediaUri, reason);
- SoftPreconditions.checkState(false, TAG, "unexpected onRecordDeleteFailed");
- }
@Override
- public void onRecordStarted(Uri mediaUri) {
- if (DEBUG) Log.d(TAG, "onRecordStarted " + mediaUri);
- super.onRecordStarted(mediaUri);
- mState = State.RECORDING_STARTED;
- updateRecording(Recording.buildFrom(mRecording)
- .setState(Recording.STATE_RECORDING_IN_PROGRESS)
- .build());
+ public void onRecordingStopped(Uri recordedProgramUri) {
+ super.onRecordingStopped(recordedProgramUri);
+ mState = State.CONNECTED;
+ updateRecording(ScheduledRecording.buildFrom(mScheduledRecording)
+ .setState(ScheduledRecording.STATE_RECORDING_FINISHED).build());
+ sendRemove();
}
@Override
- public void onRecordStopped(Uri mediaUri, @TvRecording.RecordStopReason int reason) {
- if (DEBUG) Log.d(TAG, "onRecordStopped " + mediaUri + " reason " + reason);
- super.onRecordStopped(mediaUri, reason);
+ public void onError(int reason) {
+ if (DEBUG) Log.d(TAG, "onError reason " + reason);
+ super.onError(reason);
// TODO(dvr) handle success
switch (reason) {
default:
- updateRecording(Recording.buildFrom(mRecording)
- .setState(Recording.STATE_RECORDING_FAILED)
+ updateRecording(ScheduledRecording.buildFrom(mScheduledRecording)
+ .setState(ScheduledRecording.STATE_RECORDING_FAILED)
.build());
}
release();
@@ -179,67 +171,78 @@ class RecordingTask extends TvRecording.ClientCallback implements Handler.Callba
}
private void handleInit() {
+ if (DEBUG) Log.d(TAG, "handleInit " + mScheduledRecording);
//TODO check recording preconditions
- Channel channel = mRecording.getChannel();
- if (channel == null) {
- Log.w(TAG, "Null channel for " + mRecording);
+
+ if (mScheduledRecording.getEndTimeMs() < mClock.currentTimeMillis()) {
+ Log.w(TAG, "End time already past, not recording " + mScheduledRecording);
failAndQuit();
return;
}
- String inputId = channel.getInputId();
- if (mSessionManager.canAcquireDvrSession(inputId, channel)) {
- mSession = mSessionManager.acquireDvrSession(inputId, channel);
- mState = State.SESSION_ACQUIRED;
- } else {
- Log.w(TAG, "Unable to acquire a session for " + mRecording);
+ if (mChannel == null) {
+ Log.w(TAG, "Null channel for " + mScheduledRecording);
+ failAndQuit();
+ return;
+ }
+ if (mChannel.getId() != mScheduledRecording.getChannelId()) {
+ Log.w(TAG, "Channel" + mChannel + " does not match scheduled recording "
+ + mScheduledRecording);
failAndQuit();
return;
}
- mSession.connect(inputId, this);
- mState = State.CONNECTION_PENDING;
-
- if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_START_RECORDING,
- mRecording.getStartTimeMs() - MS_BEFORE_START)) {
- mState = State.ERROR;
+ String inputId = mChannel.getInputId();
+ if (mSessionManager.canAcquireDvrSession(inputId, mChannel)) {
+ mTvRecordingClient = mSessionManager
+ .createTvRecordingClient("recordingTask-" + mScheduledRecording.getId(), this,
+ mHandler);
+ mState = State.SESSION_ACQUIRED;
+ } else {
+ Log.w(TAG, "Unable to acquire a session for " + mScheduledRecording);
+ failAndQuit();
return;
}
+ mDvrManager.addListener(this, mHandler);
+ mTvRecordingClient.tune(inputId, mChannel.getUri());
+ mState = State.CONNECTION_PENDING;
}
private void failAndQuit() {
- updateRecordingState(Recording.STATE_RECORDING_FAILED);
+ if (DEBUG) Log.d(TAG, "failAndQuit");
+ updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED);
mState = State.ERROR;
sendRemove();
}
private void sendRemove() {
+ if (DEBUG) Log.d(TAG, "sendRemove");
if (mHandler != null) {
mHandler.sendEmptyMessage(Scheduler.HandlerWrapper.MESSAGE_REMOVE);
}
}
private void handleStartRecording() {
- if (DEBUG)Log.d(TAG, "handleStartRecording " + mRecording);
+ if (DEBUG) Log.d(TAG, "handleStartRecording " + mScheduledRecording);
// TODO(DVR) handle errors
- Channel channel = mRecording.getChannel();
- mSession.startRecord(channel.getUri(), getIdAsMediaUri(mRecording));
- mState= State.RECORDING_START_REQUESTED;
+ long programId = mScheduledRecording.getProgramId();
+ mTvRecordingClient.startRecording(programId == ScheduledRecording.ID_NOT_SET ? null
+ : TvContract.buildProgramUri(programId));
+ updateRecording(ScheduledRecording.buildFrom(mScheduledRecording)
+ .setState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS).build());
+ mState = State.RECORDING_STARTED;
+
if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_STOP_RECORDING,
- mRecording.getEndTimeMs() + MS_AFTER_END)) {
+ mScheduledRecording.getEndTimeMs() + MS_AFTER_END)) {
mState = State.ERROR;
return;
}
}
private void handleStopRecording() {
- if (DEBUG)Log.d(TAG, "handleStopRecording " + mRecording);
- mSession.stopRecord();
- // TODO: once we add an API to notify successful completion of recording,
- // the following parts need to be moved to the listener implementation.
- updateRecording(Recording.buildFrom(mRecording)
- .setState(Recording.STATE_RECORDING_FINISHED).build());
- sendRemove();
+ if (DEBUG) Log.d(TAG, "handleStopRecording " + mScheduledRecording);
+ mTvRecordingClient.stopRecording();
+ mState = State.RECORDING_STOP_REQUESTED;
}
@VisibleForTesting
@@ -248,10 +251,10 @@ class RecordingTask extends TvRecording.ClientCallback implements Handler.Callba
}
private void release() {
- if (mSession != null) {
- mSession.release();
- mSessionManager.releaseDvrSession(mSession);
+ if (mTvRecordingClient != null) {
+ mSessionManager.releaseTvRecordingClient(mTvRecordingClient);
}
+ mDvrManager.removeListener(this);
}
private boolean sendEmptyMessageAtAbsoluteTime(int what, long when) {
@@ -264,28 +267,55 @@ class RecordingTask extends TvRecording.ClientCallback implements Handler.Callba
return mHandler.sendEmptyMessageDelayed(what, delay);
}
- private void updateRecordingState(@Recording.RecordingState int state) {
- updateRecording(Recording.buildFrom(mRecording).setState(state).build());
+ private void updateRecordingState(@ScheduledRecording.RecordingState int state) {
+ updateRecording(ScheduledRecording.buildFrom(mScheduledRecording).setState(state).build());
}
- @VisibleForTesting static Uri getIdAsMediaUri(Recording recording) {
+ @VisibleForTesting
+ static Uri getIdAsMediaUri(ScheduledRecording scheduledRecording) {
// TODO define the URI format
- return new Uri.Builder().appendPath(String.valueOf(recording.getId())).build();
+ return new Uri.Builder().appendPath(String.valueOf(scheduledRecording.getId())).build();
}
- private void updateRecording(Recording updatedRecording) {
- if (DEBUG) Log.d(TAG, "updateRecording " + updatedRecording);
- mRecording = updatedRecording;
+ private void updateRecording(ScheduledRecording updatedScheduledRecording) {
+ if (DEBUG) Log.d(TAG, "updateScheduledRecording " + updatedScheduledRecording);
+ mScheduledRecording = updatedScheduledRecording;
mMainThreadHandler.post(new Runnable() {
@Override
public void run() {
- mDataManager.updateRecording(mRecording);
+ mDataManager.updateScheduledRecording(mScheduledRecording);
}
});
}
@Override
+ public void onStopRecordingRequested(ScheduledRecording recording) {
+ if (recording.getId() != mScheduledRecording.getId()) {
+ return;
+ }
+ switch (mState) {
+ case RECORDING_STARTED:
+ mHandler.removeMessages(MESSAGE_STOP_RECORDING);
+ handleStopRecording();
+ break;
+ case RECORDING_STOP_REQUESTED:
+ // Do nothing
+ break;
+ case NOT_STARTED:
+ case SESSION_ACQUIRED:
+ case CONNECTION_PENDING:
+ case CONNECTED:
+ case RECORDING_START_REQUESTED:
+ case ERROR:
+ case RELEASED:
+ default:
+ sendRemove();
+ break;
+ }
+ }
+
+ @Override
public String toString() {
- return getClass().getName() + "(" + mRecording + ")";
+ return getClass().getName() + "(" + mScheduledRecording + ")";
}
}
diff --git a/src/com/android/tv/dvr/ScheduledProgramReaper.java b/src/com/android/tv/dvr/ScheduledProgramReaper.java
new file mode 100644
index 00000000..9053eaec
--- /dev/null
+++ b/src/com/android/tv/dvr/ScheduledProgramReaper.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr;
+
+import android.support.annotation.MainThread;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.tv.util.Clock;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Deletes {@link ScheduledRecording} older than {@value @DAYS} days.
+ */
+class ScheduledProgramReaper implements Runnable {
+
+ @VisibleForTesting
+ static final int DAYS = 2;
+ private final WritableDvrDataManager mDvrDataManager;
+ private final Clock mClock;
+
+ ScheduledProgramReaper(WritableDvrDataManager dvrDataManager, Clock clock) {
+ mDvrDataManager = dvrDataManager;
+ mClock = clock;
+ }
+
+ @Override
+ @MainThread
+ public void run() {
+ List<ScheduledRecording> recordings = mDvrDataManager.getAllScheduledRecordings();
+ long cutoff = mClock.currentTimeMillis() - TimeUnit.DAYS.toMillis(DAYS);
+ for (ScheduledRecording r : recordings) {
+ if (r.getEndTimeMs() < cutoff) {
+ mDvrDataManager.removeScheduledRecording(r);
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/Recording.java b/src/com/android/tv/dvr/ScheduledRecording.java
index 9ecda4da..01b00459 100644
--- a/src/com/android/tv/dvr/Recording.java
+++ b/src/com/android/tv/dvr/ScheduledRecording.java
@@ -16,49 +16,44 @@
package com.android.tv.dvr;
+import android.content.ContentValues;
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.common.SoftPreconditions;
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 {
+public final class ScheduledRecording {
private static final String TAG = "Recording";
- public static final String RECORDING_ID_EXTRA = "extra.dvr.recording.id";
+ public static final String RECORDING_ID_EXTRA = "extra.dvr.recording.id"; //TODO(DVR) move
public static final String PARAM_INPUT_ID = "input_id";
public static final long ID_NOT_SET = -1;
- public static final Comparator<Recording> START_TIME_COMPARATOR = new Comparator<Recording>() {
+ public static final Comparator<ScheduledRecording> START_TIME_COMPARATOR = new Comparator<ScheduledRecording>() {
@Override
- public int compare(Recording lhs, Recording rhs) {
+ public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
return Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs);
}
};
- public static final Comparator<Recording> PRIORITY_COMPARATOR = new Comparator<Recording>() {
+ public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR = new Comparator<ScheduledRecording>() {
@Override
- public int compare(Recording lhs, Recording rhs) {
+ public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
int value = Long.compare(lhs.mPriority, rhs.mPriority);
if (value == 0) {
value = Long.compare(lhs.mId, rhs.mId);
@@ -67,10 +62,10 @@ public final class Recording {
}
};
- public static final Comparator<Recording> START_TIME_THEN_PRIORITY_COMPARATOR
- = new Comparator<Recording>() {
+ public static final Comparator<ScheduledRecording> START_TIME_THEN_PRIORITY_COMPARATOR
+ = new Comparator<ScheduledRecording>() {
@Override
- public int compare(Recording lhs, Recording rhs) {
+ public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
int value = START_TIME_COMPARATOR.compare(lhs, rhs);
if (value == 0) {
value = PRIORITY_COMPARATOR.compare(lhs, rhs);
@@ -79,18 +74,15 @@ public final class Recording {
}
};
- public static Builder builder(Channel c, Program p) {
+ public static Builder builder(Program p) {
return new Builder()
- .setChannel(c)
- .setStartTime(p.getStartTimeUtcMillis())
- .setEndTime(p.getEndTimeUtcMillis())
- .setPrograms(Collections.singletonList(p))
+ .setStartTime(p.getStartTimeUtcMillis()).setEndTime(p.getEndTimeUtcMillis())
+ .setProgramId(p.getId())
.setType(TYPE_PROGRAM);
}
- public static Builder builder(Channel c, long startTime, long endTime) {
+ public static Builder builder(long startTime, long endTime) {
return new Builder()
- .setChannel(c)
.setStartTime(startTime)
.setEndTime(endTime)
.setType(TYPE_TIMED);
@@ -99,13 +91,11 @@ public final class Recording {
public static final class Builder {
private long mId = ID_NOT_SET;
private long mPriority = Long.MAX_VALUE;
- private Uri mUri;
- private Channel mChannel;
- private List<Program> mPrograms;
+ private long mChannelId;
+ private long mProgramId = ID_NOT_SET;
private @RecordingType int mType;
private long mStartTime;
private long mEndTime;
- private long mSize;
private @RecordingState int mState;
private SeasonRecording mParentSeasonRecording;
@@ -121,18 +111,13 @@ public final class Recording {
return this;
}
- private Builder setUri(Uri uri) {
- mUri = uri;
+ public Builder setChannelId(long channelId) {
+ mChannelId = channelId;
return this;
}
- private Builder setChannel(Channel channel) {
- mChannel = channel;
- return this;
- }
-
- public Builder setPrograms(List<Program> programs) {
- mPrograms = programs;
+ public Builder setProgramId(long programId) {
+ mProgramId = programId;
return this;
}
@@ -151,11 +136,6 @@ public final class Recording {
return this;
}
- public Builder setSize(long size) {
- mSize = size;
- return this;
- }
-
public Builder setState(@RecordingState int state) {
mState = state;
return this;
@@ -166,28 +146,21 @@ public final class Recording {
return this;
}
- public Recording build() {
- return new Recording(mId, mPriority, mUri, mChannel, mPrograms, mType, mStartTime,
- mEndTime, mSize,
- mState, mParentSeasonRecording);
+ public ScheduledRecording build() {
+ return new ScheduledRecording(mId, mPriority, mChannelId, mProgramId, mType, mStartTime,
+ mEndTime, mState, mParentSeasonRecording);
}
}
/**
* Creates {@link Builder} object from the given original {@code Recording}.
*/
- public static Builder buildFrom(Recording orig) {
+ public static Builder buildFrom(ScheduledRecording 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)
- .setType(orig.mType)
- .setUri(orig.mUri);
+ .setId(orig.mId).setChannelId(orig.mChannelId)
+ .setEndTime(orig.mEndTimeMs).setParentSeasonRecording(orig.mParentSeasonRecording)
+ .setProgramId(orig.mProgramId)
+ .setStartTime(orig.mStartTimeMs).setState(orig.mState).setType(orig.mType);
}
@Retention(RetentionPolicy.SOURCE)
@@ -196,6 +169,7 @@ public final class Recording {
public @interface RecordingState {}
public static final int STATE_RECORDING_NOT_STARTED = 0;
public static final int STATE_RECORDING_IN_PROGRESS = 1;
+ @Deprecated // It is not used.
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;
@@ -215,20 +189,46 @@ public final class Recording {
@RecordingType private final int mType;
/**
- * Use this projection if you want to create {@link Recording} object using {@link #fromCursor}.
+ * Use this projection if you want to create {@link ScheduledRecording} object using {@link #fromCursor}.
*/
public static final String[] PROJECTION = {
- // Columns must match what is read in Recording.fromCursor()
- DvrContract.Recordings._ID,
- DvrContract.Recordings.COLUMN_PRIORITY,
- DvrContract.Recordings.COLUMN_TYPE,
- DvrContract.Recordings.COLUMN_URI,
- DvrContract.Recordings.COLUMN_CHANNEL_ID,
- DvrContract.Recordings.COLUMN_START_TIME_UTC_MILLIS,
- DvrContract.Recordings.COLUMN_END_TIME_UTC_MILLIS,
- DvrContract.Recordings.COLUMN_MEDIA_SIZE,
- DvrContract.Recordings.COLUMN_STATE
- };
+ // Columns must match what is read in Recording.fromCursor()
+ DvrContract.Recordings._ID,
+ DvrContract.Recordings.COLUMN_PRIORITY,
+ DvrContract.Recordings.COLUMN_TYPE,
+ DvrContract.Recordings.COLUMN_CHANNEL_ID,
+ DvrContract.Recordings.COLUMN_PROGRAM_ID,
+ DvrContract.Recordings.COLUMN_START_TIME_UTC_MILLIS,
+ DvrContract.Recordings.COLUMN_END_TIME_UTC_MILLIS,
+ DvrContract.Recordings.COLUMN_STATE};
+ /**
+ * Creates {@link ScheduledRecording} object from the given {@link Cursor}.
+ */
+ public static ScheduledRecording fromCursor(Cursor c) {
+ int index = -1;
+ return new Builder()
+ .setId(c.getLong(++index))
+ .setPriority(c.getLong(++index))
+ .setType(recordingType(c.getString(++index)))
+ .setChannelId(c.getLong(++index))
+ .setProgramId(c.getLong(++index))
+ .setStartTime(c.getLong(++index))
+ .setEndTime(c.getLong(++index))
+ .setState(recordingState(c.getString(++index)))
+ .build();
+ }
+
+ public static ContentValues toContentValues(ScheduledRecording r) {
+ ContentValues values = new ContentValues();
+ values.put(DvrContract.Recordings.COLUMN_CHANNEL_ID, r.getChannelId());
+ values.put(DvrContract.Recordings.COLUMN_PROGRAM_ID, r.getProgramId());
+ values.put(DvrContract.Recordings.COLUMN_PRIORITY, r.getPriority());
+ values.put(DvrContract.Recordings.COLUMN_START_TIME_UTC_MILLIS, r.getStartTimeMs());
+ values.put(DvrContract.Recordings.COLUMN_END_TIME_UTC_MILLIS, r.getEndTimeMs());
+ values.put(DvrContract.Recordings.COLUMN_STATE, r.getState());
+ values.put(DvrContract.Recordings.COLUMN_TYPE, r.getType());
+ return values;
+ }
/**
* The ID internal to Live TV
@@ -243,53 +243,30 @@ public final class Recording {
*/
private final long mPriority;
- /**
- * The {@link Uri} is used as its identifier with the TIS.
- * Note: If the state is STATE_RECORDING_NOT_STARTED, this might be {@code null}.
- */
- @Nullable
- private final Uri mUri;
+ private final long mChannelId;
/**
- * 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.
+ * Optional id of the associated program.
+ *
*/
- @NonNull
- private final List<Program> mPrograms;
+ private final long mProgramId;
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, long priority, Uri uri, Channel channel, List<Program> programs,
- @RecordingType int type, long startTime, long endTime, long size,
+ private ScheduledRecording(long id, long priority, long channelId, long programId,
+ @RecordingType int type, long startTime, long endTime,
@RecordingState int state, SeasonRecording parentSeasonRecording) {
mId = id;
mPriority = priority;
- if (uri == null && id >= 0 && channel != null) {
- uri = new Uri.Builder()
- .scheme("record")
- .authority("com.android.tv")
- .appendPath(Long.toString(mId))
- .appendQueryParameter(PARAM_INPUT_ID, channel.getInputId())
- .build();
- }
- mUri = uri;
- mChannel = channel;
- mPrograms = programs == null ? Collections.EMPTY_LIST : new ArrayList<>(programs);
+ mChannelId = channelId;
+ mProgramId = programId;
mType = type;
mStartTimeMs = startTime;
mEndTimeMs = endTime;
- mMediaSize = size;
mState = state;
mParentSeasonRecording = parentSeasonRecording;
}
@@ -304,24 +281,17 @@ public final class Recording {
}
/**
- * Returns {@link android.net.Uri} representing the recording.
- */
- public Uri getUri() {
- return mUri;
- }
-
- /**
* Returns recorded {@link Channel}.
*/
- public Channel getChannel() {
- return mChannel;
+ public long getChannelId() {
+ return mChannelId;
}
/**
- * Returns a list of recorded {@link Program}.
+ * Return the optional program id
*/
- public List<Program> getPrograms() {
- return mPrograms;
+ public long getProgramId() {
+ return mProgramId;
}
/**
@@ -346,13 +316,6 @@ public final class Recording {
}
/**
- * 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}.
*/
@@ -376,30 +339,6 @@ public final class Recording {
}
/**
- * 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.setPriority(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) {
@@ -459,7 +398,7 @@ public final class Recording {
@Override
public String toString() {
- return "Recording[" + mId
+ return "ScheduledRecording[" + mId
+ "]"
+ "(startTime=" + Utils.toIsoDateTimeString(mStartTimeMs)
+ ",endTime=" + Utils.toIsoDateTimeString(mEndTimeMs)
diff --git a/src/com/android/tv/dvr/Scheduler.java b/src/com/android/tv/dvr/Scheduler.java
index 8070f8a6..ff9bde68 100644
--- a/src/com/android/tv/dvr/Scheduler.java
+++ b/src/com/android/tv/dvr/Scheduler.java
@@ -28,6 +28,8 @@ import android.util.Log;
import android.util.LongSparseArray;
import android.util.Range;
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
import com.android.tv.util.Clock;
import java.util.List;
@@ -37,7 +39,7 @@ import java.util.concurrent.TimeUnit;
* The core class to manage schedule and run actual recording.
*/
@VisibleForTesting
-public class Scheduler implements DvrDataManager.Listener {
+public class Scheduler implements DvrDataManager.ScheduledRecordingListener {
private static final String TAG = "Scheduler";
private static final boolean DEBUG = false;
@@ -51,9 +53,9 @@ public class Scheduler implements DvrDataManager.Listener {
public static final int MESSAGE_REMOVE = 999;
private final long mId;
- HandlerWrapper(Looper looper, Recording recording, RecordingTask recordingTask) {
+ HandlerWrapper(Looper looper, ScheduledRecording scheduledRecording, RecordingTask recordingTask) {
super(looper, recordingTask);
- mId = recording.getId();
+ mId = scheduledRecording.getId();
}
@Override
@@ -73,27 +75,32 @@ public class Scheduler implements DvrDataManager.Listener {
private final Looper mLooper;
private final DvrSessionManager mSessionManager;
private final WritableDvrDataManager mDataManager;
+ private final DvrManager mDvrManager;
+ private final ChannelDataManager mChannelDataManager;
private final Context mContext;
private final Clock mClock;
private final AlarmManager mAlarmManager;
- public Scheduler(Looper looper, DvrSessionManager sessionManager,
- WritableDvrDataManager dataManager, Context context, Clock clock,
+ public Scheduler(Looper looper, DvrManager dvrManager, DvrSessionManager sessionManager,
+ WritableDvrDataManager dataManager, ChannelDataManager channelDataManager,
+ Context context, Clock clock,
AlarmManager alarmManager) {
mLooper = looper;
+ mDvrManager = dvrManager;
mSessionManager = sessionManager;
mDataManager = dataManager;
+ mChannelDataManager = channelDataManager;
mContext = context;
mClock = clock;
mAlarmManager = alarmManager;
}
private void updatePendingRecordings() {
- List<Recording> recordings = mDataManager.getRecordingsThatOverlapWith(
+ List<ScheduledRecording> scheduledRecordings = mDataManager.getRecordingsThatOverlapWith(
new Range(mClock.currentTimeMillis(),
mClock.currentTimeMillis() + SOON_DURATION_IN_MS));
// TODO(DVR): handle removing and updating exiting recordings.
- for (Recording r : recordings) {
+ for (ScheduledRecording r : scheduledRecordings) {
scheduleRecordingSoon(r);
}
}
@@ -108,18 +115,18 @@ public class Scheduler implements DvrDataManager.Listener {
}
@Override
- public void onRecordingAdded(Recording recording) {
- if (DEBUG) Log.d(TAG, "added " + recording);
- if (startsWithin(recording, SOON_DURATION_IN_MS)) {
- scheduleRecordingSoon(recording);
+ public void onScheduledRecordingAdded(ScheduledRecording scheduledRecording) {
+ if (DEBUG) Log.d(TAG, "added " + scheduledRecording);
+ if (startsWithin(scheduledRecording, SOON_DURATION_IN_MS)) {
+ scheduleRecordingSoon(scheduledRecording);
} else {
updateNextAlarm();
}
}
@Override
- public void onRecordingRemoved(Recording recording) {
- long id = recording.getId();
+ public void onScheduledRecordingRemoved(ScheduledRecording scheduledRecording) {
+ long id = scheduledRecording.getId();
HandlerWrapper wrapper = mPendingRecordings.get(id);
if (wrapper != null) {
wrapper.removeCallbacksAndMessages(null);
@@ -130,16 +137,18 @@ public class Scheduler implements DvrDataManager.Listener {
}
@Override
- public void onRecordingStatusChanged(Recording recording) {
+ public void onScheduledRecordingStatusChanged(ScheduledRecording scheduledRecording) {
//TODO(DVR): implement
}
- private void scheduleRecordingSoon(Recording recording) {
- RecordingTask recordingTask = new RecordingTask(recording, mSessionManager, mDataManager,
- mClock);
- HandlerWrapper handlerWrapper = new HandlerWrapper(mLooper, recording, recordingTask);
+ private void scheduleRecordingSoon(ScheduledRecording scheduledRecording) {
+ Channel channel = mChannelDataManager.getChannel(scheduledRecording.getChannelId());
+ RecordingTask recordingTask = new RecordingTask(scheduledRecording, channel, mDvrManager,
+ mSessionManager, mDataManager, mClock);
+ HandlerWrapper handlerWrapper = new HandlerWrapper(mLooper, scheduledRecording,
+ recordingTask);
recordingTask.setHandler(handlerWrapper);
- mPendingRecordings.put(recording.getId(), handlerWrapper);
+ mPendingRecordings.put(scheduledRecording.getId(), handlerWrapper);
handlerWrapper.sendEmptyMessage(RecordingTask.MESSAGE_INIT);
}
@@ -164,7 +173,7 @@ public class Scheduler implements DvrDataManager.Listener {
}
@VisibleForTesting
- boolean startsWithin(Recording recording, long durationInMs) {
- return mClock.currentTimeMillis() >= recording.getStartTimeMs() - durationInMs;
+ boolean startsWithin(ScheduledRecording scheduledRecording, long durationInMs) {
+ return mClock.currentTimeMillis() >= scheduledRecording.getStartTimeMs() - durationInMs;
}
}
diff --git a/src/com/android/tv/dvr/SeasonRecording.java b/src/com/android/tv/dvr/SeasonRecording.java
index 074ef017..7f89e135 100644
--- a/src/com/android/tv/dvr/SeasonRecording.java
+++ b/src/com/android/tv/dvr/SeasonRecording.java
@@ -29,7 +29,7 @@ public class SeasonRecording {
*/
private static final int ALL_SEASON = -1;
- private List<Recording> mSchedule;
+ private List<ScheduledRecording> 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
index 87809701..0b8a4c99 100644
--- a/src/com/android/tv/dvr/WritableDvrDataManager.java
+++ b/src/com/android/tv/dvr/WritableDvrDataManager.java
@@ -29,7 +29,7 @@ interface WritableDvrDataManager extends DvrDataManager {
/**
* Add a new recording.
*/
- void addRecording(Recording recording);
+ void addScheduledRecording(ScheduledRecording scheduledRecording);
/**
* Add a season recording/
@@ -39,7 +39,7 @@ interface WritableDvrDataManager extends DvrDataManager {
/**
* Remove a recording.
*/
- void removeRecording(Recording Recording);
+ void removeScheduledRecording(ScheduledRecording ScheduledRecording);
/**
* Remove a season schedule.
@@ -49,5 +49,5 @@ interface WritableDvrDataManager extends DvrDataManager {
/**
* Update an existing recording.
*/
- void updateRecording(Recording r);
+ void updateScheduledRecording(ScheduledRecording r);
}
diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
index 3fc6e4a9..6058aa54 100644
--- a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
+++ b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
@@ -21,19 +21,12 @@ import android.database.Cursor;
import android.os.AsyncTask;
import android.support.annotation.Nullable;
-import com.android.tv.data.Channel;
-import com.android.tv.data.Program;
-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.ScheduledRecording;
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;
@@ -87,14 +80,14 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result>
* The id will be -1 if there was an error.
*/
public abstract static class AsyncAddRecordingTask
- extends AsyncDvrDbTask<Recording, Void, List<Recording>> {
+ extends AsyncDvrDbTask<ScheduledRecording, Void, List<ScheduledRecording>> {
public AsyncAddRecordingTask(Context context) {
super(context);
}
@Override
- protected final List<Recording> doInDvrBackground(Recording... params) {
+ protected final List<ScheduledRecording> doInDvrBackground(ScheduledRecording... params) {
return sDbHelper.insertRecordings(params);
}
}
@@ -106,13 +99,13 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result>
* if no match was found. The count is expected to be exactly 1 for each recording.
*/
public abstract static class AsyncUpdateRecordingTask
- extends AsyncDvrDbTask<Recording, Void, List<Integer>> {
+ extends AsyncDvrDbTask<ScheduledRecording, Void, List<Integer>> {
public AsyncUpdateRecordingTask(Context context) {
super(context);
}
@Override
- protected final List<Integer> doInDvrBackground(Recording... params) {
+ protected final List<Integer> doInDvrBackground(ScheduledRecording... params) {
return sDbHelper.updateRecordings(params);
}
}
@@ -124,91 +117,43 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result>
* if no match was found. The count is expected to be exactly 1 for each recording.
*/
public abstract static class AsyncDeleteRecordingTask
- extends AsyncDvrDbTask<Recording, Void, List<Integer>> {
+ extends AsyncDvrDbTask<ScheduledRecording, Void, List<Integer>> {
public AsyncDeleteRecordingTask(Context context) {
super(context);
}
@Override
- protected final List<Integer> doInDvrBackground(Recording... params) {
+ protected final List<Integer> doInDvrBackground(ScheduledRecording... params) {
return sDbHelper.deleteRecordings(params);
}
}
public abstract static class AsyncDvrQueryTask
- extends AsyncDvrDbTask<Void, Void, List<Recording>> {
+ extends AsyncDvrDbTask<Void, Void, List<ScheduledRecording>> {
public AsyncDvrQueryTask(Context context) {
super(context);
}
@Override
@Nullable
- protected final List<Recording> doInDvrBackground(Void... params) {
+ protected final List<ScheduledRecording> doInDvrBackground(Void... params) {
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<>();
- recordingToProgramMap.put(recordingId, programList);
- }
- 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);
+ List<ScheduledRecording> scheduledRecordings = new ArrayList<>();
+ try (Cursor c = sDbHelper.query(Recordings.TABLE_NAME, ScheduledRecording.PROJECTION)) {
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));
+ scheduledRecordings.add(ScheduledRecording.fromCursor(c));
}
}
- return recordings;
+ return scheduledRecordings;
}
}
}
diff --git a/src/com/android/tv/dvr/provider/DvrContract.java b/src/com/android/tv/dvr/provider/DvrContract.java
index e6ce4141..192cc17b 100644
--- a/src/com/android/tv/dvr/provider/DvrContract.java
+++ b/src/com/android/tv/dvr/provider/DvrContract.java
@@ -73,22 +73,23 @@ public final class DvrContract {
public static final String COLUMN_TYPE = "type";
/**
- * The URI string for the recorded media.
+ * The ID of the channel for recording.
*
- * <p>This field can be null if the media is not recorded yet.
+ * <p>This is a required field.
*
- * <p>Type: String
+ * <p>Type: INTEGER (long)
*/
- public static final String COLUMN_URI = "uri";
+ public static final String COLUMN_CHANNEL_ID = "channel_id";
+
/**
- * The ID of the channel for recording.
+ * The ID of the associated program for recording.
*
- * <p>This is a required field. It's not an ID in TvProvider, but in DVR database.
+ * <p>This is an optional field.
*
* <p>Type: INTEGER (long)
*/
- public static final String COLUMN_CHANNEL_ID = "channel_id";
+ public static final String COLUMN_PROGRAM_ID = "program_id";
/**
* The start time of this recording, in milliseconds since the epoch.
@@ -109,13 +110,6 @@ public final class DvrContract {
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},
@@ -127,49 +121,9 @@ public final class DvrContract {
* <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";
+ private Recordings() { }
}
- /**
- * 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";
- }
+ private DvrContract() { }
}
diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
index 2445e935..bdba8ac3 100644
--- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
+++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
@@ -24,11 +24,7 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.util.Log;
-import com.android.tv.data.Channel;
-import com.android.tv.dvr.Recording;
-import com.android.tv.dvr.provider.DvrContract.DvrChannels;
-import com.android.tv.dvr.provider.DvrContract.DvrPrograms;
-import com.android.tv.dvr.provider.DvrContract.RecordingToPrograms;
+import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.dvr.provider.DvrContract.Recordings;
import java.util.ArrayList;
@@ -41,49 +37,22 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = "DvrDatabaseHelper";
private static final boolean DEBUG = true;
- private static final int DATABASE_VERSION = 2;
+ private static final int DATABASE_VERSION = 4;
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_PRIORITY
- + " INTEGER DEFAULT " + Long.MAX_VALUE + ","
+ + Recordings._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + Recordings.COLUMN_PRIORITY + " INTEGER DEFAULT " + Long.MAX_VALUE + ","
+ Recordings.COLUMN_TYPE + " TEXT NOT NULL,"
- + Recordings.COLUMN_URI + " TEXT,"
+ Recordings.COLUMN_CHANNEL_ID + " INTEGER NOT NULL,"
+ + Recordings.COLUMN_PROGRAM_ID + " INTEGER ,"
+ 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 static final String WHERE_RECORDING_ID_EQUALS = Recordings._ID + " = ?";
public DvrDatabaseHelper(Context context) {
@@ -99,22 +68,10 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
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);
@@ -135,15 +92,15 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
*
* @return The list of recordings with id set. The id will be -1 if there was an error.
*/
- public List<Recording> insertRecordings(Recording... recordings) {
- updateChannelsFromRecordings(recordings);
+ public List<ScheduledRecording> insertRecordings(ScheduledRecording... scheduledRecordings) {
+ updateChannelsFromRecordings(scheduledRecordings);
SQLiteDatabase db = getReadableDatabase();
- List<Recording> results = new ArrayList<>();
- for (Recording r : recordings) {
- ContentValues values = getContentValues(r);
+ List<ScheduledRecording> results = new ArrayList<>();
+ for (ScheduledRecording r : scheduledRecordings) {
+ ContentValues values = ScheduledRecording.toContentValues(r);
long id = db.insert(Recordings.TABLE_NAME, null, values);
- results.add(Recording.buildFrom(r).setId(id).build());
+ results.add(ScheduledRecording.buildFrom(r).setId(id).build());
}
return results;
}
@@ -154,13 +111,12 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
* @return The list of row update counts. The count will be -1 if there was an error or 0
* if no match was found. The count is expected to be exactly 1 for each recording.
*/
- public List<Integer> updateRecordings(Recording[] recordings) {
- updateChannelsFromRecordings(recordings);
+ public List<Integer> updateRecordings(ScheduledRecording[] scheduledRecordings) {
+ updateChannelsFromRecordings(scheduledRecordings);
SQLiteDatabase db = getWritableDatabase();
List<Integer> results = new ArrayList<>();
- long count = 0;
- for (Recording r : recordings) {
- ContentValues values = getContentValues(r);
+ for (ScheduledRecording r : scheduledRecordings) {
+ ContentValues values = ScheduledRecording.toContentValues(r);
int updated = db.update(Recordings.TABLE_NAME, values, Recordings._ID + " = ?",
new String[] {String.valueOf(r.getId())});
results.add(updated);
@@ -168,42 +124,21 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
return results;
}
- private void updateChannelsFromRecordings(Recording[] recordings) {
+ private void updateChannelsFromRecordings(ScheduledRecording[] scheduledRecordings) {
// TODO(DVR) implement/
// TODO(DVR) consider not deleting channels instead of keeping a separate table.
}
- private ContentValues getContentValues(Recording r) {
- ContentValues values = new ContentValues();
- // TODO(DVR): use DVR channel id instead
- Channel channel = r.getChannel();
- if (channel != null) {
- values.put(Recordings.COLUMN_CHANNEL_ID, channel.getId());
- }
- values.put(Recordings.COLUMN_PRIORITY, r.getPriority());
- values.put(Recordings.COLUMN_START_TIME_UTC_MILLIS, r.getStartTimeMs());
- values.put(Recordings.COLUMN_END_TIME_UTC_MILLIS, r.getEndTimeMs());
- values.put(Recordings.COLUMN_STATE, r.getState());
- values.put(Recordings.COLUMN_MEDIA_SIZE, r.getSize());
- values.put(Recordings.COLUMN_TYPE, r.getType());
- if (r.getUri() != null) {
- values.put(Recordings.COLUMN_URI, r.getUri().toString());
- }
- return values;
- }
-
/**
* Delete recordings.
*
* @return The list of row update counts. The count will be -1 if there was an error or 0
* if no match was found. The count is expected to be exactly 1 for each recording.
*/
- public List<Integer> deleteRecordings(Recording[] recordings) {
+ public List<Integer> deleteRecordings(ScheduledRecording[] scheduledRecordings) {
SQLiteDatabase db = getWritableDatabase();
List<Integer> results = new ArrayList<>();
- long count = 0;
- for (Recording r : recordings) {
- ContentValues values = getContentValues(r);
+ for (ScheduledRecording r : scheduledRecordings) {
int deleted = db.delete(Recordings.TABLE_NAME, WHERE_RECORDING_ID_EQUALS,
new String[] {String.valueOf(r.getId())});
results.add(deleted);
diff --git a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java
index 87e47930..70e71cab 100644
--- a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java
@@ -20,26 +20,33 @@ import android.os.Bundle;
import android.support.annotation.IntDef;
import android.support.v17.leanback.app.BrowseFragment;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.ObjectAdapter;
import android.util.Log;
import com.android.tv.R;
import com.android.tv.TvApplication;
+import com.android.tv.common.recording.RecordedProgram;
import com.android.tv.dvr.DvrDataManager;
-import com.android.tv.dvr.Recording;
+import com.android.tv.dvr.ScheduledRecording;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.LinkedHashMap;
-import java.util.List;
/**
* {@link BrowseFragment} for DVR functions.
*/
public class DvrBrowseFragment extends BrowseFragment {
private static final String TAG = "DvrBrowseFragment";
+ private static final boolean DEBUG = false;
+
+ private ScheduledRecordingsAdapter mRecordingsInProgressAdapter;
+ private ScheduledRecordingsAdapter mRecordingsNotStatedAdapter;
+ private RecordedProgramsAdapter mRecordedProgramsAdapter;
@IntDef({DVR_CURRENT_RECORDINGS, DVR_SCHEDULED_RECORDINGS, DVR_RECORDED_PROGRAMS, DVR_SETTINGS})
@Retention(RetentionPolicy.SOURCE)
@@ -49,27 +56,48 @@ public class DvrBrowseFragment extends BrowseFragment {
public static final int DVR_RECORDED_PROGRAMS = 2;
public static final int DVR_SETTINGS = 3;
- private static LinkedHashMap<Integer, Integer> sHeaders =
+ private static final LinkedHashMap<Integer, Integer> sHeaders =
new LinkedHashMap<Integer, Integer>() {{
put(DVR_CURRENT_RECORDINGS, R.string.dvr_main_current_recordings);
put(DVR_SCHEDULED_RECORDINGS, R.string.dvr_main_scheduled_recordings);
put(DVR_RECORDED_PROGRAMS, R.string.dvr_main_recorded_programs);
- put(DVR_SETTINGS, R.string.dvr_main_settings);
+ /* put(DVR_SETTINGS, R.string.dvr_main_settings); */ // TODO: Temporarily remove it for DP.
}};
private DvrDataManager mDvrDataManager;
private ArrayObjectAdapter mRowsAdapter;
@Override
- public void onActivityCreated(Bundle savedInstanceState) {
- Log.d(TAG, "onCreate");
- super.onActivityCreated(savedInstanceState);
+ public void onCreate(Bundle savedInstanceState) {
+ if (DEBUG) Log.d(TAG, "onCreate");
+ super.onCreate(savedInstanceState);
+ mDvrDataManager = TvApplication.getSingletons(getContext()).getDvrDataManager();
setupUiElements();
- setupAdapter();
+ setupAdapters();
+ mRecordingsInProgressAdapter.start();
+ mRecordingsNotStatedAdapter.start();
+ mRecordedProgramsAdapter.start();
+ initRows();
prepareEntranceTransition();
+ startEntranceTransition();
+ }
- // TODO: load asynchronously.
- loadData();
+ @Override
+ public void onStart() {
+ if (DEBUG) Log.d(TAG, "onStart");
+ super.onStart();
+ // TODO: It's a workaround for a bug that a progress bar isn't hidden.
+ // We need to remove it later.
+ getProgressBarManager().disableProgressBar();
+ }
+
+ @Override
+ public void onDestroy() {
+ if (DEBUG) Log.d(TAG, "onDestroy");
+ mRecordingsInProgressAdapter.stop();
+ mRecordingsNotStatedAdapter.stop();
+ mRecordedProgramsAdapter.stop();
+ super.onDestroy();
}
private void setupUiElements() {
@@ -77,43 +105,51 @@ public class DvrBrowseFragment extends BrowseFragment {
setHeadersTransitionOnBackEnabled(false);
}
- private void setupAdapter() {
- mDvrDataManager = TvApplication.getSingletons(getContext()).getDvrDataManager();
+ private void setupAdapters() {
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
setAdapter(mRowsAdapter);
+ ClassPresenterSelector presenterSelector = new ClassPresenterSelector();
+ EmptyItemPresenter emptyItemPresenter = new EmptyItemPresenter(this);
+ ScheduledRecordingPresenter scheduledRecordingPresenter = new ScheduledRecordingPresenter(
+ getContext());
+ RecordedProgramPresenter recordedProgramPresenter = new RecordedProgramPresenter(
+ getContext());
+ presenterSelector.addClassPresenter(ScheduledRecording.class, scheduledRecordingPresenter);
+ presenterSelector.addClassPresenter(RecordedProgram.class, recordedProgramPresenter);
+ presenterSelector.addClassPresenter(EmptyHolder.class, emptyItemPresenter);
+ mRecordingsInProgressAdapter = new ScheduledRecordingsAdapter(mDvrDataManager,
+ ScheduledRecording.STATE_RECORDING_IN_PROGRESS, presenterSelector);
+ mRecordingsNotStatedAdapter = new ScheduledRecordingsAdapter(mDvrDataManager,
+ ScheduledRecording.STATE_RECORDING_NOT_STARTED, presenterSelector);
+ mRecordedProgramsAdapter = new RecordedProgramsAdapter(mDvrDataManager, presenterSelector);
}
- private void loadRow(ArrayObjectAdapter gridRowAdapter, List<Recording> recordings) {
- if (recordings == null || recordings.size() == 0) {
- gridRowAdapter.add(null);
- return;
- }
- for (Recording r : recordings) {
- gridRowAdapter.add(r);
- }
- }
-
- private void loadData() {
+ private void initRows() {
+ mRowsAdapter.clear();
for (@DVR_HEADERS_MODE int i : sHeaders.keySet()) {
HeaderItem gridHeader = new HeaderItem(i, getContext().getString(sHeaders.get(i)));
- GridItemPresenter gridPresenter = new GridItemPresenter(this);
- ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(gridPresenter);
+ ObjectAdapter gridRowAdapter = null;
switch (i) {
- case DVR_CURRENT_RECORDINGS:
- loadRow(gridRowAdapter, mDvrDataManager.getStartedRecordings());
+ case DVR_CURRENT_RECORDINGS: {
+ gridRowAdapter = mRecordingsInProgressAdapter;
break;
- case DVR_SCHEDULED_RECORDINGS:
- loadRow(gridRowAdapter, mDvrDataManager.getScheduledRecordings());
+ }
+ case DVR_SCHEDULED_RECORDINGS: {
+ gridRowAdapter = mRecordingsNotStatedAdapter;
+ }
break;
- case DVR_RECORDED_PROGRAMS:
- loadRow(gridRowAdapter, mDvrDataManager.getFinishedRecordings());
+ case DVR_RECORDED_PROGRAMS: {
+ gridRowAdapter = mRecordedProgramsAdapter;
+ }
break;
case DVR_SETTINGS:
+ gridRowAdapter = new ArrayObjectAdapter(new EmptyItemPresenter(this));
// TODO: provide setup rows.
break;
}
- mRowsAdapter.add(new ListRow(gridHeader, gridRowAdapter));
+ if (gridRowAdapter != null) {
+ mRowsAdapter.add(new ListRow(gridHeader, gridRowAdapter));
+ }
}
- startEntranceTransition();
}
}
diff --git a/src/com/android/tv/dvr/ui/DvrDialogFragment.java b/src/com/android/tv/dvr/ui/DvrDialogFragment.java
new file mode 100644
index 00000000..38de9d8d
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrDialogFragment.java
@@ -0,0 +1,50 @@
+package com.android.tv.dvr.ui;
+
+import android.app.FragmentManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v17.leanback.app.GuidedStepFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.tv.MainActivity;
+import com.android.tv.R;
+import com.android.tv.guide.ProgramGuide;
+
+public class DvrDialogFragment extends HalfSizedDialogFragment {
+ private final DvrGuidedStepFragment mDvrGuidedStepFragment;
+
+ public DvrDialogFragment(DvrGuidedStepFragment dvrGuidedStepFragment) {
+ mDvrGuidedStepFragment = dvrGuidedStepFragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ ProgramGuide programGuide =
+ ((MainActivity) getActivity()).getOverlayManager().getProgramGuide();
+ if (programGuide != null && programGuide.isActive()) {
+ programGuide.cancelHide();
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+ FragmentManager fm = getChildFragmentManager();
+ GuidedStepFragment.add(fm, mDvrGuidedStepFragment, R.id.halfsized_dialog_host);
+ return view;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ ProgramGuide programGuide =
+ ((MainActivity) getActivity()).getOverlayManager().getProgramGuide();
+ if (programGuide != null && programGuide.isActive()) {
+ programGuide.scheduleHide();
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
new file mode 100644
index 00000000..0854b91a
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
@@ -0,0 +1,73 @@
+package com.android.tv.dvr.ui;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v17.leanback.app.GuidedStepFragment;
+import android.support.v17.leanback.widget.GuidanceStylist;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.tv.MainActivity;
+import com.android.tv.TvApplication;
+import com.android.tv.dialog.SafeDismissDialogFragment;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.guide.ProgramManager.TableEntry;
+import com.android.tv.R;
+
+public class DvrGuidedStepFragment extends GuidedStepFragment {
+ private final TableEntry mEntry;
+ private DvrManager mDvrManager;
+
+ public DvrGuidedStepFragment(TableEntry entry) {
+ mEntry = entry;
+ }
+
+ protected TableEntry getEntry() {
+ return mEntry;
+ }
+
+ protected DvrManager getDvrManager() {
+ return mDvrManager;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mDvrManager = TvApplication.getSingletons(context).getDvrManager();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+ VerticalGridView gridView = getGuidedActionsStylist().getActionsGridView();
+ gridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE);
+ return view;
+ }
+
+ @Override
+ public GuidanceStylist onCreateGuidanceStylist() {
+ // Workaround: b/28448653
+ return new GuidanceStylist() {
+ @Override
+ public int onProvideLayoutId() {
+ return R.layout.halfsized_guidance;
+ }
+ };
+ }
+
+ @Override
+ public int onProvideTheme() {
+ return R.style.Theme_TV_Dvr_GuidedStep;
+ }
+
+ protected void dismissDialog() {
+ SafeDismissDialogFragment currentDialog =
+ ((MainActivity) getActivity()).getOverlayManager().getCurrentDialog();
+ if (currentDialog instanceof DvrDialogFragment) {
+ currentDialog.dismiss();
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrRecordConflictFragment.java b/src/com/android/tv/dvr/ui/DvrRecordConflictFragment.java
new file mode 100644
index 00000000..92052b5b
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrRecordConflictFragment.java
@@ -0,0 +1,82 @@
+package com.android.tv.dvr.ui;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.tv.MainActivity;
+import com.android.tv.R;
+
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.Program;
+import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.guide.ProgramManager.TableEntry;
+
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.List;
+
+public class DvrRecordConflictFragment extends DvrGuidedStepFragment {
+ private static final int DVR_EPG_RECORD = 1;
+ private static final int DVR_EPG_NOT_RECORD = 2;
+
+ private List<ScheduledRecording> mConflicts;
+
+ public DvrRecordConflictFragment(TableEntry entry) {
+ super(entry);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mConflicts = getDvrManager().getScheduledRecordingsThatConflict(getEntry().program);
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ final MainActivity tvActivity = (MainActivity) getActivity();
+ final ChannelDataManager channelDataManager = tvActivity.getChannelDataManager();
+ StringBuilder sb = new StringBuilder();
+ for (ScheduledRecording r : mConflicts) {
+ Channel channel = channelDataManager.getChannel(r.getChannelId());
+ if (channel == null) {
+ continue;
+ }
+ sb.append(channel.getDisplayName())
+ .append(" : ")
+ .append(DateFormat.getDateTimeInstance().format(new Date(r.getStartTimeMs())))
+ .append("\n");
+ }
+ String title = getResources().getString(R.string.dvr_epg_conflict_dialog_title);
+ String description = sb.toString();
+ return new Guidance(title, description, null, null);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ Activity activity = getActivity();
+ actions.add(new GuidedAction.Builder(activity)
+ .id(DVR_EPG_RECORD)
+ .title(getResources().getString(R.string.dvr_epg_record))
+ .build());
+ actions.add(new GuidedAction.Builder(activity)
+ .id(DVR_EPG_NOT_RECORD)
+ .title(getResources().getString(R.string.dvr_epg_do_not_record))
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ Program program = getEntry().program;
+ if (action.getId() == DVR_EPG_RECORD) {
+ getDvrManager().addSchedule(program, mConflicts);
+ }
+ dismissDialog();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrRecordDeleteFragment.java b/src/com/android/tv/dvr/ui/DvrRecordDeleteFragment.java
new file mode 100644
index 00000000..d4d5cc41
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrRecordDeleteFragment.java
@@ -0,0 +1,48 @@
+package com.android.tv.dvr.ui;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+
+import com.android.tv.R;
+import com.android.tv.guide.ProgramManager.TableEntry;
+
+import java.util.List;
+
+public class DvrRecordDeleteFragment extends DvrGuidedStepFragment {
+ private static final int ACTION_DELETE_YES = 1;
+ private static final int ACTION_DELETE_NO = 2;
+
+ public DvrRecordDeleteFragment(TableEntry entry) {
+ super(entry);
+ }
+
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getResources().getString(R.string.epg_dvr_dialog_message_delete_schedule);
+ return new Guidance(title, null, null, null);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ Activity activity = getActivity();
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_DELETE_YES)
+ .title(getResources().getString(android.R.string.yes))
+ .build());
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_DELETE_NO)
+ .title(getResources().getString(android.R.string.no))
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ if (action.getId() == ACTION_DELETE_YES) {
+ getDvrManager().removeScheduledRecording(getEntry().scheduledRecording);
+ }
+ dismissDialog();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrRecordScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrRecordScheduleFragment.java
new file mode 100644
index 00000000..77e78ccc
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrRecordScheduleFragment.java
@@ -0,0 +1,70 @@
+package com.android.tv.dvr.ui;
+
+import android.app.Activity;
+import android.app.FragmentManager;
+import android.os.Bundle;
+
+import android.support.v17.leanback.app.GuidedStepFragment;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+
+import com.android.tv.data.Program;
+import com.android.tv.dialog.SafeDismissDialogFragment;
+import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.guide.ProgramManager.TableEntry;
+import com.android.tv.MainActivity;
+import com.android.tv.R;
+
+import java.util.List;
+
+public class DvrRecordScheduleFragment extends DvrGuidedStepFragment {
+ private static final int ACTION_RECORD_YES = 1;
+ private static final int ACTION_RECORD_NO = 2;
+
+ public DvrRecordScheduleFragment(TableEntry entry) {
+ super(entry);
+ }
+
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getResources().getString(R.string.epg_dvr_dialog_message_schedule_recording);
+ return new Guidance(title, null, null, null);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ Activity activity = getActivity();
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_RECORD_YES)
+ .title(getResources().getString(android.R.string.yes))
+ .build());
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_RECORD_NO)
+ .title(getResources().getString(android.R.string.no))
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ TableEntry entry = getEntry();
+ Program program = entry.program;
+ final List<ScheduledRecording> conflicts =
+ getDvrManager().getScheduledRecordingsThatConflict(program);
+ if (action.getId() == ACTION_RECORD_YES) {
+ if (conflicts.isEmpty()) {
+ getDvrManager().addSchedule(program, conflicts);
+ dismissDialog();
+ } else {
+ DvrRecordConflictFragment dvrConflict = new DvrRecordConflictFragment(entry);
+ SafeDismissDialogFragment currentDialog =
+ ((MainActivity) getActivity()).getOverlayManager().getCurrentDialog();
+ if (currentDialog instanceof DvrDialogFragment) {
+ FragmentManager fm = currentDialog.getChildFragmentManager();
+ GuidedStepFragment.add(fm, dvrConflict, R.id.halfsized_dialog_host);
+ }
+ }
+ } else if (action.getId() == ACTION_RECORD_NO) {
+ dismissDialog();
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/EmptyHolder.java b/src/com/android/tv/dvr/ui/EmptyHolder.java
new file mode 100644
index 00000000..45cd3a36
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/EmptyHolder.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+/**
+ * Special object meaning a row is empty;
+ */
+final class EmptyHolder {
+ static final EmptyHolder EMPTY_HOLDER = new EmptyHolder();
+
+ private EmptyHolder() {
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/EmptyItemPresenter.java b/src/com/android/tv/dvr/ui/EmptyItemPresenter.java
new file mode 100644
index 00000000..c0305128
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/EmptyItemPresenter.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.support.v17.leanback.widget.Presenter;
+import android.view.Gravity;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.util.Utils;
+
+/**
+ * Shows the item "NONE". Used for rows with now items.
+ */
+public class EmptyItemPresenter extends Presenter {
+
+ private final DvrBrowseFragment mMainFragment;
+
+ public EmptyItemPresenter(DvrBrowseFragment mainFragment) {
+ mMainFragment = mainFragment;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ TextView view = new TextView(parent.getContext());
+ Resources resources = view.getResources();
+ view.setLayoutParams(new ViewGroup.LayoutParams(
+ resources.getDimensionPixelSize(R.dimen.dvr_card_layout_width),
+ resources.getDimensionPixelSize(R.dimen.dvr_card_layout_width)));
+ view.setFocusable(true);
+ view.setFocusableInTouchMode(true);
+ view.setBackgroundColor(
+ Utils.getColor(mMainFragment.getResources(), R.color.setup_background));
+ view.setTextColor(Color.WHITE);
+ view.setGravity(Gravity.CENTER);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder viewHolder, Object recording) {
+ ((TextView) viewHolder.view).setText(
+ viewHolder.view.getContext().getString(R.string.dvr_msg_no_recording_on_the_row));
+ }
+
+ @Override
+ public void onUnbindViewHolder(ViewHolder viewHolder) { }
+}
diff --git a/src/com/android/tv/dvr/ui/GridItemPresenter.java b/src/com/android/tv/dvr/ui/GridItemPresenter.java
deleted file mode 100644
index 099816d4..00000000
--- a/src/com/android/tv/dvr/ui/GridItemPresenter.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.dvr.ui;
-
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.graphics.Color;
-import android.support.v17.leanback.widget.Presenter;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import com.android.tv.MainActivity;
-import com.android.tv.R;
-import com.android.tv.TvApplication;
-import com.android.tv.data.Channel;
-import com.android.tv.data.Program;
-import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.Recording;
-import com.android.tv.util.Utils;
-
-import java.util.List;
-
-public class GridItemPresenter extends Presenter {
- private static final int GRID_ITEM_WIDTH = 200;
- private static final int GRID_ITEM_HEIGHT = 200;
-
- private final DvrBrowseFragment mainFragment;
-
- public GridItemPresenter(DvrBrowseFragment mainFragment) {
- this.mainFragment = mainFragment;
- }
-
- @Override
- public ViewHolder onCreateViewHolder(ViewGroup parent) {
- TextView view = new TextView(parent.getContext());
- view.setLayoutParams(new ViewGroup.LayoutParams(GRID_ITEM_WIDTH, GRID_ITEM_HEIGHT));
- view.setFocusable(true);
- view.setFocusableInTouchMode(true);
- view.setBackgroundColor(
- Utils.getColor(mainFragment.getResources(), R.color.setup_background));
- view.setTextColor(Color.WHITE);
- view.setGravity(Gravity.CENTER);
- return new ViewHolder(view);
- }
-
- @Override
- public void onBindViewHolder(ViewHolder viewHolder, Object recording) {
- if (recording == null) {
- ((TextView) viewHolder.view).setText(viewHolder.view.getContext()
- .getString(R.string.dvr_msg_no_recording_on_the_row));
- } else {
- final Recording r = (Recording) recording;
- StringBuilder sb = new StringBuilder();
- List<Program> programs = r.getPrograms();
- if (programs != null && programs.size() > 0) {
- sb.append(programs.get(0).getTitle());
- } else {
- sb.append(viewHolder.view.getContext()
- .getString(R.string.dvr_msg_program_title_unknown));
- }
- sb.append(" ");
- Channel channel = r.getChannel();
- if (channel != null) {
- sb.append(channel.getDisplayName());
- } else {
- sb.append(viewHolder.view.getContext().getString(R.string.dvr_msg_channel_unknown));
- }
- sb.append(" ").append(Utils.toIsoDateTimeString(r.getStartTimeMs()));
- ((TextView) viewHolder.view).setText(sb.toString());
- final Context context = viewHolder.view.getContext();
- viewHolder.view.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- switch (r.getState()) {
- case Recording.STATE_RECORDING_NOT_STARTED: {
- new AlertDialog.Builder(context)
- .setNegativeButton(R.string.dvr_detail_cancel,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- Toast.makeText(context, "Not implemented yet",
- Toast.LENGTH_SHORT).show();
- }
- })
- .show();
- break;
- }
- case Recording.STATE_RECORDING_IN_PROGRESS: {
- new AlertDialog.Builder(context)
- .setNegativeButton(R.string.dvr_detail_stop_delete,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- Toast.makeText(context, "Not implemented yet",
- Toast.LENGTH_SHORT).show();
- }
- })
- .setPositiveButton(R.string.dvr_detail_stop_keep,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- Toast.makeText(context, "Not implemented yet",
- Toast.LENGTH_SHORT).show();
- }
- })
- .show();
- break;
- }
- case Recording.STATE_RECORDING_FINISHED: {
- new AlertDialog.Builder(context)
- .setNegativeButton(R.string.dvr_detail_delete,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- DvrManager dvrManager = TvApplication
- .getSingletons(mainFragment.getContext())
- .getDvrManager();
- // TODO(DVR) handle success/failure.
- dvrManager.removeRecording(r);
- }
- })
- .setPositiveButton(R.string.dvr_detail_play,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- Intent intent = new Intent(context, MainActivity.class);
- intent.putExtra(Utils.EXTRA_KEY_RECORDING_URI,
- r.getUri());
- context.startActivity(intent);
- ((Activity) context).finish();
- }
- })
- .show();
- break;
- }
- }
- }
- });
- }
- }
-
- @Override
- public void onUnbindViewHolder(ViewHolder viewHolder) {
- }
-} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java
new file mode 100644
index 00000000..dc89a8e0
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java
@@ -0,0 +1,30 @@
+package com.android.tv.dvr.ui;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.tv.dialog.SafeDismissDialogFragment;
+import com.android.tv.R;
+
+public class HalfSizedDialogFragment extends SafeDismissDialogFragment {
+ public static final String DIALOG_TAG = HalfSizedDialogFragment.class.getSimpleName();
+ public static final String TRACKER_LABEL = "Half sized dialog";
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.halfsized_dialog, null);
+ }
+
+ @Override
+ public int getTheme() {
+ return R.style.Theme_TV_dialog_HalfSizedDialog;
+ }
+
+ @Override
+ public String getTrackerLabel() {
+ return TRACKER_LABEL;
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java
new file mode 100644
index 00000000..0b656bdc
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.media.tv.TvContract;
+import android.support.v17.leanback.widget.Presenter;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.List;
+
+import com.android.tv.MainActivity;
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.common.recording.RecordedProgram;
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.ui.DialogUtils;
+import com.android.tv.util.Utils;
+
+/**
+ * Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}.
+ */
+public class RecordedProgramPresenter extends Presenter {
+ private final ChannelDataManager mChannelDataManager;
+
+ public RecordedProgramPresenter(Context context) {
+ mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager();
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ Context context = parent.getContext();
+ RecordingCardView view = new RecordingCardView(context);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder viewHolder, Object o) {
+ final RecordedProgram recording = (RecordedProgram) o;
+ final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
+ final Context context = viewHolder.view.getContext();
+ final Resources resources = context.getResources();
+
+ Channel channel = mChannelDataManager.getChannel(recording.getChannelId());
+
+ if (!TextUtils.isEmpty(recording.getTitle())) {
+ cardView.setTitle(recording.getTitle());
+ } else {
+ cardView.setTitle(resources.getString(R.string.dvr_msg_program_title_unknown));
+ }
+ if (recording.getPosterArt() != null) {
+ cardView.setImageUri(recording.getPosterArt());
+ } else if (recording.getThumbnail() != null) {
+ cardView.setImageUri(recording.getThumbnail());
+ } else {
+ if (channel != null) {
+ cardView.setImageUri(TvContract.buildChannelLogoUri(channel.getId()).toString());
+ }
+ }
+ cardView.setContent(Utils.getDurationString(context, recording.getStartTimeUtcMillis(),
+ recording.getEndTimeUtcMillis(), true));
+ //TODO: replace with a detail card
+ viewHolder.view.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ DialogUtils.showListDialog(v.getContext(),
+ new int[] { R.string.dvr_detail_play, R.string.dvr_detail_delete },
+ new Runnable[] {
+ new Runnable() {
+ @Override
+ public void run() {
+ Intent intent = new Intent(context, MainActivity.class);
+ intent.putExtra(Utils.EXTRA_KEY_RECORDING_URI,
+ recording.getUri());
+ context.startActivity(intent);
+ ((Activity) context).finish();
+ }
+ },
+ new Runnable() {
+ @Override
+ public void run() {
+ DvrManager dvrManager = TvApplication
+ .getSingletons(context).getDvrManager();
+ dvrManager.removeRecordedProgram(recording);
+ }
+ },
+ });
+ }
+ });
+
+ }
+
+ @Override
+ public void onUnbindViewHolder(ViewHolder viewHolder) {
+ final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
+ cardView.reset();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/RecordedProgramsAdapter.java b/src/com/android/tv/dvr/ui/RecordedProgramsAdapter.java
new file mode 100644
index 00000000..eeb26041
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/RecordedProgramsAdapter.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.support.v17.leanback.widget.PresenterSelector;
+
+import com.android.tv.common.recording.RecordedProgram;
+import com.android.tv.dvr.DvrDataManager;
+
+/**
+ * Adapter for {@link RecordedProgram}.
+ */
+final class RecordedProgramsAdapter extends SortedArrayAdapter<RecordedProgram>
+ implements DvrDataManager.RecordedProgramListener {
+ private final DvrDataManager mDataManager;
+
+ RecordedProgramsAdapter(DvrDataManager dataManager, PresenterSelector presenterSelector) {
+ super(presenterSelector, RecordedProgram.START_TIME_THEN_ID_COMPARATOR);
+ mDataManager = dataManager;
+ }
+
+ public void start() {
+ clear();
+ addAll(mDataManager.getRecordedPrograms());
+ mDataManager.addRecordedProgramListener(this);
+ }
+
+ public void stop() {
+ mDataManager.removeRecordedProgramListener(this);
+ }
+
+ @Override
+ long getId(RecordedProgram item) {
+ return item.getId();
+ }
+
+ @Override // DvrDataManager.RecordedProgramListener
+ public void onRecordedProgramAdded(RecordedProgram recordedProgram) {
+ add(recordedProgram);
+ }
+
+ @Override // DvrDataManager.RecordedProgramListener
+ public void onRecordedProgramChanged(RecordedProgram recordedProgram) {
+ change(recordedProgram);
+ }
+
+ @Override // DvrDataManager.RecordedProgramListener
+ public void onRecordedProgramRemoved(RecordedProgram recordedProgram) {
+ remove(recordedProgram);
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/RecordingCardView.java b/src/com/android/tv/dvr/ui/RecordingCardView.java
new file mode 100644
index 00000000..def11248
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/RecordingCardView.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.widget.BaseCardView;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.tv.R;
+import com.android.tv.util.ImageLoader;
+
+/**
+ * A CardView for displaying info about a {@link com.android.tv.dvr.ScheduledRecording} or
+ * {@link com.android.tv.common.recording.RecordedProgram}
+ */
+class RecordingCardView extends BaseCardView {
+ private final ImageView mImageView;
+ private final int mImageWidth;
+ private final int mImageHeight;
+ private String mImageUri;
+ private final TextView mTitleView;
+ private final TextView mContentView;
+ private final Drawable mDefaultImage;
+
+ RecordingCardView(Context context) {
+ super(context);
+ //TODO(dvr): move these to the layout XML.
+ setCardType(BaseCardView.CARD_TYPE_INFO_UNDER_WITH_EXTRA);
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ mDefaultImage = getResources().getDrawable(R.drawable.default_now_card, null);
+
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ inflater.inflate(R.layout.dvr_recording_card_view, this);
+
+ mImageView = (ImageView) findViewById(R.id.image);
+ mImageWidth = getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_width);
+ mImageHeight = getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_width);
+ mTitleView = (TextView) findViewById(R.id.title);
+ mContentView = (TextView) findViewById(R.id.content);
+ }
+
+ void setTitle(CharSequence title) {
+ mTitleView.setText(title);
+ }
+
+ void setContent(CharSequence content) {
+ mContentView.setText(content);
+ }
+
+ void setImageUri(String uri) {
+ mImageUri = uri;
+ if (TextUtils.isEmpty(uri)) {
+ mImageView.setImageDrawable(mDefaultImage);
+ } else {
+ ImageLoader.loadBitmap(getContext(), uri, mImageWidth, mImageHeight,
+ new RecordingCardImageLoaderCallback(this, uri));
+ }
+ }
+
+ public void setImageUri(Uri uri) {
+ if (uri != null) {
+ setImageUri(uri.toString());
+ } else {
+ setImageUri("");
+ }
+ }
+
+ private static class RecordingCardImageLoaderCallback
+ extends ImageLoader.ImageLoaderCallback<RecordingCardView> {
+ private final String mUri;
+
+ RecordingCardImageLoaderCallback(RecordingCardView referent, String uri) {
+ super(referent);
+ mUri = uri;
+ }
+
+ @Override
+ public void onBitmapLoaded(RecordingCardView view, @Nullable Bitmap bitmap) {
+ if (bitmap == null || !mUri.equals(view.mImageUri)) {
+ view.mImageView.setImageDrawable(view.mDefaultImage);
+ } else {
+ view.mImageView.setImageDrawable(new BitmapDrawable(view.getResources(), bitmap));
+ }
+ }
+ }
+
+ public void reset() {
+ mTitleView.setText("");
+ mContentView.setText("");
+ mImageView.setImageDrawable(mDefaultImage);
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java
new file mode 100644
index 00000000..533a4882
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.media.tv.TvContract;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.widget.Presenter;
+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.TvApplication;
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.Program;
+import com.android.tv.data.ProgramDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.util.Utils;
+
+/**
+ * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}.
+ */
+public class ScheduledRecordingPresenter extends Presenter {
+ private final ChannelDataManager mChannelDataManager;
+
+ private static final class ScheduledRecordingViewHolder extends ViewHolder {
+ private ProgramDataManager.QueryProgramTask mQueryProgramTask;
+
+ ScheduledRecordingViewHolder(RecordingCardView view) {
+ super(view);
+ }
+ }
+
+ public ScheduledRecordingPresenter(Context context) {
+ ApplicationSingletons singletons = TvApplication.getSingletons(context);
+ mChannelDataManager = singletons.getChannelDataManager();
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ Context context = parent.getContext();
+ RecordingCardView view = new RecordingCardView(context);
+ return new ScheduledRecordingViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder baseHolder, Object o) {
+ ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder;
+ final ScheduledRecording recording = (ScheduledRecording) o;
+ final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
+ final Context context = viewHolder.view.getContext();
+
+ long programId = recording.getProgramId();
+ if (programId == ScheduledRecording.ID_NOT_SET) {
+ setTitleAndImage(cardView, recording, null);
+ } else {
+ viewHolder.mQueryProgramTask = new ProgramDataManager.QueryProgramTask(
+ context.getContentResolver(), programId) {
+ @Override
+ protected void onPostExecute(Program program) {
+ super.onPostExecute(program);
+ setTitleAndImage(cardView, recording, program);
+ }
+ };
+ viewHolder.mQueryProgramTask.executeOnDbThread();
+
+ }
+ cardView.setContent(Utils.getDurationString(context, recording.getStartTimeMs(),
+ recording.getEndTimeMs(), true));
+ //TODO: replace with a detail card
+ View.OnClickListener clickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ switch (recording.getState()) {
+ case ScheduledRecording.STATE_RECORDING_NOT_STARTED: {
+ showScheduledRecordingDialog(v.getContext(), recording);
+ break;
+ }
+ case ScheduledRecording.STATE_RECORDING_IN_PROGRESS: {
+ showCurrentlyRecordingDialog(v.getContext(), recording);
+ break;
+ }
+ }
+ }
+ };
+ baseHolder.view.setOnClickListener(clickListener);
+ }
+
+ private void setTitleAndImage(RecordingCardView cardView, ScheduledRecording recording,
+ @Nullable Program program) {
+ if (program != null) {
+ cardView.setTitle(program.getTitle());
+ cardView.setImageUri(program.getPosterArtUri());
+ } else {
+ cardView.setTitle(
+ cardView.getResources().getString(R.string.dvr_msg_program_title_unknown));
+ Channel channel = mChannelDataManager.getChannel(recording.getChannelId());
+ if (channel != null) {
+ cardView.setImageUri(TvContract.buildChannelLogoUri(channel.getId()).toString());
+ }
+ }
+ }
+
+ @Override
+ public void onUnbindViewHolder(ViewHolder baseHolder) {
+ ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder;
+ final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
+ if (viewHolder.mQueryProgramTask != null) {
+ viewHolder.mQueryProgramTask.cancel(true);
+ viewHolder.mQueryProgramTask = null;
+ }
+ cardView.reset();
+ }
+
+ private void showScheduledRecordingDialog(final Context context,
+ final ScheduledRecording recording) {
+ DialogInterface.OnClickListener removeScheduleListener
+ = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // TODO(DVR) handle success/failure.
+ DvrManager dvrManager = TvApplication.getSingletons(context)
+ .getDvrManager();
+ dvrManager.removeScheduledRecording((ScheduledRecording) recording);
+ }
+ };
+ new AlertDialog.Builder(context)
+ .setMessage(R.string.epg_dvr_dialog_message_remove_recording_schedule)
+ .setNegativeButton(android.R.string.no, null)
+ .setPositiveButton(android.R.string.yes, removeScheduleListener)
+ .show();
+ }
+
+ private void showCurrentlyRecordingDialog(final Context context,
+ final ScheduledRecording recording) {
+ DialogInterface.OnClickListener stopRecordingListener
+ = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ DvrManager dvrManager = TvApplication.getSingletons(context)
+ .getDvrManager();
+ dvrManager.stopRecording((ScheduledRecording) recording);
+ }
+ };
+ new AlertDialog.Builder(context)
+ .setMessage(R.string.epg_dvr_dialog_message_stop_recording)
+ .setNegativeButton(android.R.string.no, null)
+ .setPositiveButton(android.R.string.yes, stopRecordingListener)
+ .show();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingsAdapter.java b/src/com/android/tv/dvr/ui/ScheduledRecordingsAdapter.java
new file mode 100644
index 00000000..65955276
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/ScheduledRecordingsAdapter.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.support.v17.leanback.widget.PresenterSelector;
+
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.ScheduledRecording;
+
+/**
+ * Adapter for {@link ScheduledRecording} filtered by
+ * {@link com.android.tv.dvr.ScheduledRecording.RecordingState}.
+ */
+final class ScheduledRecordingsAdapter extends SortedArrayAdapter<ScheduledRecording>
+ implements DvrDataManager.ScheduledRecordingListener {
+ private final int mState;
+ private final DvrDataManager mDataManager;
+
+ ScheduledRecordingsAdapter(DvrDataManager dataManager, int state,
+ PresenterSelector presenterSelector) {
+ super(presenterSelector, ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR);
+ mDataManager = dataManager;
+ mState = state;
+ }
+
+ public void start() {
+ clear();
+ switch (mState) {
+ case ScheduledRecording.STATE_RECORDING_NOT_STARTED:
+ addAll(mDataManager.getNonStartedScheduledRecordings());
+ break;
+ case ScheduledRecording.STATE_RECORDING_IN_PROGRESS:
+ addAll(mDataManager.getStartedRecordings());
+ break;
+ default:
+ throw new IllegalStateException("Unknown recording state " + mState);
+
+ }
+ mDataManager.addScheduledRecordingListener(this);
+ }
+
+ public void stop() {
+ mDataManager.removeScheduledRecordingListener(this);
+ }
+
+ @Override
+ long getId(ScheduledRecording item) {
+ return item.getId();
+ }
+
+ @Override //DvrDataManager.ScheduledRecordingListener
+ public void onScheduledRecordingAdded(ScheduledRecording scheduledRecording) {
+ if (scheduledRecording.getState() == mState) {
+ add(scheduledRecording);
+ }
+ }
+
+ @Override //DvrDataManager.ScheduledRecordingListener
+ public void onScheduledRecordingRemoved(ScheduledRecording scheduledRecording) {
+ remove(scheduledRecording);
+ }
+
+ @Override //DvrDataManager.ScheduledRecordingListener
+ public void onScheduledRecordingStatusChanged(ScheduledRecording scheduledRecording) {
+ if (scheduledRecording.getState() == mState) {
+ change(scheduledRecording);
+ } else {
+ remove(scheduledRecording);
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
new file mode 100644
index 00000000..8a8bcdeb
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.PresenterSelector;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Keeps a set of {@code T} items sorted, but leaving a {@link EmptyHolder}
+ * if there is no items.
+ *
+ * <p>{@code T} must have stable IDs.
+ */
+abstract class SortedArrayAdapter<T> extends ObjectAdapter {
+ private final List<T> mItems = new ArrayList<>();
+ private final Comparator<T> mComparator;
+
+ SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator) {
+ super(presenterSelector);
+ mComparator = comparator;
+ setHasStableIds(true);
+ }
+
+ @Override
+ public final int size() {
+ return mItems.isEmpty() ? 1 : mItems.size();
+ }
+
+ @Override
+ public final Object get(int position) {
+ return isEmpty() ? EmptyHolder.EMPTY_HOLDER : getItem(position);
+ }
+
+ @Override
+ public final long getId(int position) {
+ if (isEmpty()) {
+ return NO_ID;
+ }
+ T item = mItems.get(position);
+ return item == null ? NO_ID : getId(item);
+ }
+
+ /**
+ * Returns the id of the the given {@code item}.
+ *
+ * The id must be stable.
+ */
+ abstract long getId(T item);
+
+ /**
+ * Returns the item at the given {@code position}.
+ *
+ * @throws IndexOutOfBoundsException if the position is out of range
+ * (<tt>position &lt; 0 || position &gt;= size()</tt>)
+ */
+ final T getItem(int position) {
+ return mItems.get(position);
+ }
+
+ /**
+ * Returns {@code true} if the list of items is empty.
+ *
+ * <p><b>NOTE</b> when the item list is empty the adapter has a size of 1 and
+ * {@link EmptyHolder#EMPTY_HOLDER} at position 0;
+ */
+ final boolean isEmpty() {
+ return mItems.isEmpty();
+ }
+
+ /**
+ * Removes all elements from the list.
+ *
+ * <p><b>NOTE</b> when the item list is empty the adapter has a size of 1 and
+ * {@link EmptyHolder#EMPTY_HOLDER} at position 0;
+ */
+ final void clear() {
+ mItems.clear();
+ notifyChanged();
+ }
+
+ /**
+ * Adds the objects in the given collection to the adapter keeping the elements sorted.
+ * If the index is >= {@link #size} an exception will be thrown.
+ *
+ * @param items A {@link Collection} of items to insert.
+ */
+ final void addAll(Collection<T> items) {
+ mItems.addAll(items);
+ Collections.sort(mItems, mComparator);
+ notifyChanged();
+ }
+
+ /**
+ * Adds an item in sorted order to the adapter.
+ *
+ * @param item The item to add in sorted order to the adapter.
+ */
+ final void add(T item) {
+ int i = findWhereToInsert(item);
+ mItems.add(i, item);
+ if (mItems.size() == 1) {
+ notifyItemRangeChanged(0, 1);
+ } else {
+ notifyItemRangeInserted(i, 1);
+ }
+ }
+
+ /**
+ * Remove an item from the list
+ *
+ * @param item The item to remove from the adapter.
+ */
+ final void remove(T item) {
+ int index = indexOf(item);
+ if (index != -1) {
+ mItems.remove(index);
+ if (mItems.isEmpty()) {
+ notifyItemRangeChanged(0, 1);
+ } else {
+ notifyItemRangeRemoved(index, 1);
+ }
+ }
+ }
+
+ /**
+ * Change an item in the list.
+ * @param item The item to change.
+ */
+ final void change(T item) {
+ int oldIndex = indexOf(item);
+ if (oldIndex != -1) {
+ T old = mItems.get(oldIndex);
+ if (mComparator.compare(old, item) == 0) {
+ mItems.set(oldIndex, item);
+ notifyItemRangeChanged(oldIndex, 1);
+ return;
+ }
+ mItems.remove(oldIndex);
+ }
+ int newIndex = findWhereToInsert(item);
+ mItems.add(newIndex, item);
+
+ if (oldIndex != -1) {
+ notifyItemRangeRemoved(oldIndex, 1);
+ }
+ if (newIndex != -1) {
+ notifyItemRangeInserted(newIndex, 1);
+ }
+ }
+
+ private int indexOf(T item) {
+ long id = getId(item);
+ for (int i = 0; i < mItems.size(); i++) {
+ T r = mItems.get(i);
+ if (getId(r) == id) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private int findWhereToInsert(T item) {
+ int i;
+ int size = mItems.size();
+ for (i = 0; i < size; i++) {
+ T r = mItems.get(i);
+ if (mComparator.compare(r, item) > 0) {
+ return i;
+ }
+ }
+ return size;
+ }
+}
diff --git a/src/com/android/tv/guide/ProgramGrid.java b/src/com/android/tv/guide/ProgramGrid.java
index 1339ddf8..77de5827 100644
--- a/src/com/android/tv/guide/ProgramGrid.java
+++ b/src/com/android/tv/guide/ProgramGrid.java
@@ -20,6 +20,7 @@ import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v7.widget.RecyclerView.LayoutManager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
@@ -66,6 +67,16 @@ public class ProgramGrid extends VerticalGridView {
}
};
+ private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
+ new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ updateInputLogo();
+ return true;
+ }
+ };
+
private ProgramManager mProgramManager;
private View mNextFocusByUpDown;
@@ -83,7 +94,7 @@ public class ProgramGrid extends VerticalGridView {
private boolean mKeepCurrentProgram;
private ChildFocusListener mChildFocusListener;
- private OnRepeatedKeyInterceptListener mOnRepeatedKeyInterceptListener;
+ private final OnRepeatedKeyInterceptListener mOnRepeatedKeyInterceptListener;
interface ChildFocusListener {
/**
@@ -332,6 +343,10 @@ public class ProgramGrid extends VerticalGridView {
return contains((View) v.getParent());
}
+ public void onItemSelectionReset() {
+ getViewTreeObserver().addOnPreDrawListener(mPreDrawListener);
+ }
+
@Override
public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
if (mLastFocusedView != null && mLastFocusedView.isShown()) {
@@ -359,6 +374,49 @@ public class ProgramGrid extends VerticalGridView {
int maxY = (mSelectionRow + 1) * mRowHeight + mDetailHeight;
if (y > maxY) scrollBy(0, y - maxY);
}
+ updateInputLogo();
+ }
+
+ @Override
+ public void onViewRemoved(View view) {
+ // It is required to ensure input logo showing when the scroll is moved to most bottom.
+ updateInputLogo();
+ }
+
+ private int getFirstVisibleChildIndex() {
+ final LayoutManager mLayoutManager = getLayoutManager();
+ int top = mLayoutManager.getPaddingTop();
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View childView = getChildAt(i);
+ int childTop = mLayoutManager.getDecoratedTop(childView);
+ int childBottom = mLayoutManager.getDecoratedBottom(childView);
+ if ((childTop + childBottom) / 2 > top) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public void updateInputLogo() {
+ int childCount = getChildCount();
+ if (childCount == 0) {
+ return;
+ }
+ int firstVisibleChildIndex = getFirstVisibleChildIndex();
+ if (firstVisibleChildIndex == -1) {
+ return;
+ }
+ View childView = getChildAt(firstVisibleChildIndex);
+ int childAdapterPosition = getChildAdapterPosition(childView);
+ ((ProgramTableAdapter.ProgramRowHolder) getChildViewHolder(childView))
+ .updateInputLogo(childAdapterPosition, true);
+ for (int i = firstVisibleChildIndex + 1; i < childCount; i++) {
+ childView = getChildAt(i);
+ ((ProgramTableAdapter.ProgramRowHolder) getChildViewHolder(childView))
+ .updateInputLogo(childAdapterPosition, false);
+ childAdapterPosition = getChildAdapterPosition(childView);
+ }
}
private static void findFocusables(View v, ArrayList<View> outFocusable) {
diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java
index 77a1146b..bfcb8b0d 100644
--- a/src/com/android/tv/guide/ProgramGuide.java
+++ b/src/com/android/tv/guide/ProgramGuide.java
@@ -31,6 +31,7 @@ import android.os.Message;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.v17.leanback.widget.OnChildSelectedListener;
import android.support.v17.leanback.widget.SearchOrbView;
import android.support.v17.leanback.widget.VerticalGridView;
@@ -52,6 +53,7 @@ import com.android.tv.common.WeakHandler;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.GenreItems;
import com.android.tv.data.ProgramDataManager;
+import com.android.tv.dvr.DvrDataManager;
import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -160,12 +162,11 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
public ProgramGuide(MainActivity activity, ChannelTuner channelTuner,
TvInputManagerHelper tvInputManagerHelper, ChannelDataManager channelDataManager,
- ProgramDataManager programDataManager, Tracker tracker, Runnable preShowRunnable,
- Runnable postHideRunnable) {
+ ProgramDataManager programDataManager, @Nullable DvrDataManager dvrDataManager,
+ Tracker tracker, Runnable preShowRunnable, Runnable postHideRunnable) {
mActivity = activity;
- mProgramManager = new ProgramManager(tvInputManagerHelper,
- channelDataManager,
- programDataManager);
+ mProgramManager = new ProgramManager(tvInputManagerHelper, channelDataManager,
+ programDataManager, dvrDataManager);
mChannelTuner = channelTuner;
mTracker = tracker;
mPreShowRunnable = preShowRunnable;
@@ -245,7 +246,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
mTimelineRow.setAdapter(mTimeListAdapter);
ProgramTableAdapter programTableAdapter = new ProgramTableAdapter(mActivity,
- tvInputManagerHelper, mProgramManager, this);
+ mProgramManager, this);
programTableAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
@@ -590,7 +591,10 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
return mTimelineRow.getScrollOffset();
}
- private void cancelHide() {
+ /**
+ * Cancel hiding the program guide.
+ */
+ public void cancelHide() {
mHandler.removeCallbacks(mHideRunnable);
}
@@ -720,6 +724,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
Math.max(mProgramManager.getChannelIndex(mChannelTuner.getCurrentChannel()),
0));
mGrid.resetFocusState();
+ mGrid.onItemSelectionReset();
mIsDuringResetRowSelection = false;
}
diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java
index 09a93037..172ee070 100644
--- a/src/com/android/tv/guide/ProgramItemView.java
+++ b/src/com/android/tv/guide/ProgramItemView.java
@@ -16,9 +16,7 @@
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;
@@ -26,6 +24,7 @@ import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.StateListDrawable;
import android.os.Handler;
import android.os.SystemClock;
+import android.support.v4.os.BuildCompat;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
@@ -43,15 +42,14 @@ import com.android.tv.TvApplication;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
-import com.android.tv.data.Program;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.Recording;
+import com.android.tv.dvr.ui.DvrDialogFragment;
+import com.android.tv.dvr.ui.DvrRecordDeleteFragment;
+import com.android.tv.dvr.ui.DvrRecordScheduleFragment;
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 {
@@ -60,9 +58,6 @@ 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 };
@@ -89,6 +84,10 @@ public class ProgramItemView extends TextView {
@Override
public void onClick(final View view) {
TableEntry entry = ((ProgramItemView) view).mTableEntry;
+ if (entry == null) {
+ //do nothing
+ return;
+ }
ApplicationSingletons singletons = TvApplication.getSingletons(view.getContext());
Tracker tracker = singletons.getTracker();
tracker.sendEpgItemClicked();
@@ -105,78 +104,38 @@ public class ProgramItemView extends TextView {
}, entry.getWidth() > ((ProgramItemView) view).mMaxWidthForRipple ? 0
: view.getResources()
.getInteger(R.integer.program_guide_ripple_anim_duration));
- } else if (CommonFeatures.DVR.isEnabled(view.getContext())) {
+ } else if (CommonFeatures.DVR.isEnabled(view.getContext()) && BuildCompat
+ .isAtLeastN()) {
final MainActivity tvActivity = (MainActivity) view.getContext();
final DvrManager dvrManager = singletons.getDvrManager();
final Channel channel = tvActivity.getChannelDataManager()
.getChannel(entry.channelId);
- if (dvrManager.canRecord(channel.getInputId())) {
- showDvrDialog(view, entry, dvrManager);
+ if (dvrManager.canRecord(channel.getInputId()) && entry.program != null) {
+ if (entry.scheduledRecording == null) {
+ showDvrDialog(view, entry);
+ } else {
+ showRecordDeleteDialog(view, entry);
+ }
}
}
}
- private void showDvrDialog(final View view, TableEntry entry, final DvrManager dvrManager) {
- List<CharSequence> items = new ArrayList<>();
- final List<Integer> actions = new ArrayList<>();
- // TODO: the items can be changed by the state of the program. For example,
- // if the program is already added in scheduler, we need to show an item to
- // delete the recording schedule.
- items.add(view.getResources().getString(R.string.epg_dvr_record_program));
- actions.add(ACTION_RECORD_PROGRAM);
- items.add(view.getResources().getString(R.string.epg_dvr_record_season));
- actions.add(ACTION_RECORD_SEASON);
-
- final Program program = entry.program;
- final List<Recording> conflicts = dvrManager
- .getScheduledRecordingsThatConflict(program);
- // TODO: it is a tentative UI. Don't publish the UI.
- DialogInterface.OnClickListener onClickListener
- = new DialogInterface.OnClickListener() {
- @Override
- public void onClick(final DialogInterface dialog, int which) {
- if (actions.get(which) == ACTION_RECORD_PROGRAM) {
- if (conflicts.isEmpty()) {
- dvrManager.addSchedule(program, conflicts);
- } else {
- showConflictDialog(view, dvrManager, program, conflicts);
- }
- } else if (actions.get(which) == ACTION_RECORD_SEASON) {
- dvrManager.addSeasonSchedule(program);
- }
- dialog.dismiss();
- }
- };
- new AlertDialog.Builder(view.getContext())
- .setItems(items.toArray(new CharSequence[items.size()]), onClickListener)
- .create()
- .show();
+ private void showDvrDialog(final View view, TableEntry entry) {
+ Utils.showToastMessageForDeveloperFeature(view.getContext());
+ DvrRecordScheduleFragment dvrRecordScheduleFragment =
+ new DvrRecordScheduleFragment(entry);
+ DvrDialogFragment dvrDialogFragment = new DvrDialogFragment(dvrRecordScheduleFragment);
+ ((MainActivity) view.getContext()).getOverlayManager().showDialogFragment(
+ DvrDialogFragment.DIALOG_TAG, dvrDialogFragment, true, true);
}
- };
- private static void showConflictDialog(final View view, final DvrManager dvrManager,
- final Program program, final List<Recording> conflicts) {
- DialogInterface.OnClickListener conflictClickListener
- = new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- if (which == AlertDialog.BUTTON_POSITIVE) {
- dvrManager.addSchedule(program, conflicts);
- dialog.dismiss();
- }
- }
- };
- StringBuilder sb = new StringBuilder();
- for (Recording r : conflicts) {
- sb.append(r.toString()).append('\n');
+ private void showRecordDeleteDialog(final View view, final TableEntry entry) {
+ DvrRecordDeleteFragment recordDeleteDialogFragment = new DvrRecordDeleteFragment(entry);
+ DvrDialogFragment dvrDialogFragment = new DvrDialogFragment(recordDeleteDialogFragment);
+ ((MainActivity) view.getContext()).getOverlayManager().showDialogFragment(
+ DvrDialogFragment.DIALOG_TAG, dvrDialogFragment, true, true);
}
- new AlertDialog.Builder(view.getContext()).setTitle(R.string.dvr_epg_conflict_dialog_title)
- .setMessage(sb.toString())
- .setPositiveButton(R.string.dvr_epg_record, conflictClickListener)
- .setNegativeButton(R.string.dvr_epg_do_not_record, conflictClickListener)
- .create()
- .show();
- }
+ };
private static final View.OnFocusChangeListener ON_FOCUS_CHANGED =
new View.OnFocusChangeListener() {
@@ -198,6 +157,10 @@ public class ProgramItemView extends TextView {
public void run() {
refreshDrawableState();
TableEntry entry = mTableEntry;
+ if (entry == null) {
+ //do nothing
+ return;
+ }
if (entry.isCurrentProgram()) {
Drawable background = getBackground();
int progress = getProgress(entry.entryStartUtcMillis, entry.entryEndUtcMillis);
@@ -220,6 +183,8 @@ public class ProgramItemView extends TextView {
public ProgramItemView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+ setOnClickListener(ON_CLICKED);
+ setOnFocusChangeListener(ON_FOCUS_CHANGED);
}
private void initIfNeeded() {
@@ -282,11 +247,9 @@ public class ProgramItemView extends TextView {
return mTableEntry;
}
- public void onBind(TableEntry entry, ProgramListAdapter adapter) {
+ public void setValues(TableEntry entry, int selectedGenreId, long fromUtcMillis,
+ long toUtcMillis, String gapTitle) {
mTableEntry = entry;
- setOnClickListener(ON_CLICKED);
- setOnFocusChangeListener(ON_FOCUS_CHANGED);
- ProgramManager programManager = adapter.getProgramManager();
ViewGroup.LayoutParams layoutParams = getLayoutParams();
layoutParams.width = entry.getWidth();
@@ -303,16 +266,19 @@ public class ProgramItemView extends TextView {
setText(null);
} else {
if (entry.isGap()) {
- if (entry.isBlocked()) {
- title = adapter.getBlockedProgramTitle();
- } else {
- title = adapter.getNoInfoProgramTitle();
- }
+ title = gapTitle;
episode = null;
- } else if (entry.hasGenre(programManager.getSelectedGenreId())) {
+ } else if (entry.hasGenre(selectedGenreId)) {
titleStyle = sProgramTitleStyle;
episodeStyle = sEpisodeTitleStyle;
}
+ if (TextUtils.isEmpty(title)) {
+ title = getResources().getString(R.string.program_title_for_no_information);
+ }
+ if (mTableEntry.scheduledRecording != null) {
+ //TODO(dvr): use a proper icon for UI status.
+ title = "®" + title;
+ }
SpannableStringBuilder description = new SpannableStringBuilder();
description.append(title);
@@ -340,12 +306,11 @@ public class ProgramItemView extends TextView {
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
mTextWidth = getMeasuredWidth() - getPaddingStart() - getPaddingEnd();
int start = GuideUtils.convertMillisToPixel(entry.entryStartUtcMillis);
- int guideStart = GuideUtils.convertMillisToPixel(programManager.getFromUtcMillis());
+ int guideStart = GuideUtils.convertMillisToPixel(fromUtcMillis);
layoutVisibleArea(guideStart - start);
// Maximum width for us to use a ripple
- mMaxWidthForRipple = GuideUtils.convertMillisToPixel(
- programManager.getFromUtcMillis(), programManager.getToUtcMillis());
+ mMaxWidthForRipple = GuideUtils.convertMillisToPixel(fromUtcMillis, toUtcMillis);
}
/**
@@ -374,14 +339,13 @@ public class ProgramItemView extends TextView {
}
}
- public void onUnbind() {
+ public void clearValues() {
if (getHandler() != null) {
getHandler().removeCallbacks(mUpdateFocus);
}
setTag(null);
- setOnFocusChangeListener(null);
- setOnClickListener(null);
+ mTableEntry = null;
}
private static int getProgress(long start, long end) {
diff --git a/src/com/android/tv/guide/ProgramListAdapter.java b/src/com/android/tv/guide/ProgramListAdapter.java
index 88ba435e..03aea5ad 100644
--- a/src/com/android/tv/guide/ProgramListAdapter.java
+++ b/src/com/android/tv/guide/ProgramListAdapter.java
@@ -16,7 +16,6 @@
package com.android.tv.guide;
-import android.content.Context;
import android.content.res.Resources;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
@@ -33,30 +32,24 @@ import com.android.tv.guide.ProgramManager.TableEntry;
* Adapts a program list for a specific channel from {@link ProgramManager} to a row of the program
* guide table.
*/
-public class ProgramListAdapter extends
- RecyclerView.Adapter<ProgramListAdapter.ProgramViewHolder> implements
- TableEntriesUpdatedListener {
+public class ProgramListAdapter extends RecyclerView.Adapter<ProgramListAdapter.ProgramViewHolder>
+ implements TableEntriesUpdatedListener {
private static final String TAG = "ProgramListAdapter";
private static final boolean DEBUG = false;
- private final String mNoInfoProgramTitle;
- private final String mBlockedProgramTitle;
-
private final ProgramManager mProgramManager;
private final int mChannelIndex;
+ private final String mNoInfoProgramTitle;
+ private final String mBlockedProgramTitle;
private long mChannelId;
- public ProgramListAdapter(Context context, ProgramManager programManager,
- int channelIndex) {
- Resources res = context.getResources();
- mNoInfoProgramTitle = res.getString(
- R.string.program_title_for_no_information);
- mBlockedProgramTitle = res.getString(
- R.string.program_title_for_blocked_channel);
-
+ public ProgramListAdapter(Resources res, ProgramManager programManager, int channelIndex) {
+ setHasStableIds(true);
mProgramManager = programManager;
mChannelIndex = channelIndex;
+ mNoInfoProgramTitle = res.getString(R.string.program_title_for_no_information);
+ mBlockedProgramTitle = res.getString(R.string.program_title_for_blocked_channel);
onTableEntriesUpdated();
}
@@ -76,14 +69,6 @@ public class ProgramListAdapter extends
return mProgramManager;
}
- public String getNoInfoProgramTitle() {
- return mNoInfoProgramTitle;
- }
-
- public String getBlockedProgramTitle() {
- return mBlockedProgramTitle;
- }
-
@Override
public int getItemCount() {
return mProgramManager.getTableEntryCount(mChannelId);
@@ -95,8 +80,15 @@ public class ProgramListAdapter extends
}
@Override
+ public long getItemId(int position) {
+ return mProgramManager.getTableEntry(mChannelId, position).getId();
+ }
+
+ @Override
public void onBindViewHolder(ProgramViewHolder holder, int position) {
- holder.onBind(mProgramManager.getTableEntry(mChannelId, position), this);
+ TableEntry tableEntry = mProgramManager.getTableEntry(mChannelId, position);
+ String gapTitle = tableEntry.isBlocked() ? mBlockedProgramTitle : mNoInfoProgramTitle;
+ holder.onBind(tableEntry, this.getProgramManager(), gapTitle);
}
@Override
@@ -116,16 +108,16 @@ public class ProgramListAdapter extends
super(itemView);
}
- public void onBind(TableEntry entry, ProgramListAdapter adapter) {
+ public void onBind(TableEntry entry, ProgramManager programManager, String gapTitle) {
if (DEBUG) {
Log.d(TAG, "onBind. View = " + itemView + ", Entry = " + entry);
}
-
- ((ProgramItemView) itemView).onBind(entry, adapter);
+ ((ProgramItemView) itemView).setValues(entry, programManager.getSelectedGenreId(),
+ programManager.getFromUtcMillis(), programManager.getToUtcMillis(), gapTitle);
}
public void onUnbind() {
- ((ProgramItemView) itemView).onUnbind();
+ ((ProgramItemView) itemView).clearValues();
}
}
}
diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java
index df52abbe..fe1a981f 100644
--- a/src/com/android/tv/guide/ProgramManager.java
+++ b/src/com/android/tv/guide/ProgramManager.java
@@ -17,14 +17,17 @@
package com.android.tv.guide;
import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.util.ArraySet;
import android.util.Log;
-import com.android.tv.common.CollectionUtils;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.GenreItems;
import com.android.tv.data.Program;
import com.android.tv.data.ProgramDataManager;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -55,6 +58,7 @@ public class ProgramManager {
private final TvInputManagerHelper mTvInputManagerHelper;
private final ChannelDataManager mChannelDataManager;
private final ProgramDataManager mProgramDataManager;
+ private final DvrDataManager mDvrDataManager; // Only set if DVR is enabled
private long mStartUtcMillis;
private long mEndUtcMillis;
@@ -74,6 +78,8 @@ public class ProgramManager {
/** Program corresponding to the entry. {@code null} means that this entry is a gap. */
public final Program program;
+ public final ScheduledRecording scheduledRecording;
+
/** Start time of entry in UTC milliseconds. */
public final long entryStartUtcMillis;
@@ -82,34 +88,39 @@ public class ProgramManager {
private final boolean mIsBlocked;
- private TableEntry(long startUtcMillis, long endUtcMillis) {
- this(INVALID_ID, null, startUtcMillis, endUtcMillis, false);
- }
-
private TableEntry(long channelId, long startUtcMillis, long endUtcMillis) {
this(channelId, null, startUtcMillis, endUtcMillis, false);
}
private TableEntry(long channelId, long startUtcMillis, long endUtcMillis,
boolean blocked) {
- this(channelId, null, startUtcMillis, endUtcMillis, blocked);
+ this(channelId, null, null, startUtcMillis, endUtcMillis, blocked);
}
- private TableEntry(long channelId, Program program,
- long entryStartUtcMillis, long entryEndUtcMillis) {
- this(channelId, program, entryStartUtcMillis, entryEndUtcMillis, false);
+ private TableEntry(long channelId, Program program, long entryStartUtcMillis,
+ long entryEndUtcMillis, boolean isBlocked) {
+ this(channelId, program, null, entryStartUtcMillis, entryEndUtcMillis, isBlocked);
}
- private TableEntry(long channelId, Program program,
+ private TableEntry(long channelId, Program program, ScheduledRecording scheduledRecording,
long entryStartUtcMillis, long entryEndUtcMillis, boolean isBlocked) {
this.channelId = channelId;
this.program = program;
+ this.scheduledRecording = scheduledRecording;
this.entryStartUtcMillis = entryStartUtcMillis;
this.entryEndUtcMillis = entryEndUtcMillis;
mIsBlocked = isBlocked;
}
/**
+ * A stable id useful for {@link android.support.v7.widget.RecyclerView.Adapter}.
+ */
+ public long getId() {
+ // using a negative entryEndUtcMillis keeps it from conflicting with program Id
+ return program != null ? program.getId() : -entryEndUtcMillis;
+ }
+
+ /**
* Returns true if this is a gap.
*/
public boolean isGap() {
@@ -167,9 +178,10 @@ public class ProgramManager {
// Should be matched with mSelectedGenreId always.
private List<Channel> mFilteredChannels = mChannels;
- private final Set<Listener> mListeners = CollectionUtils.createSmallSet();
- private final Set<TableEntriesUpdatedListener> mTableEntriesUpdatedListeners = CollectionUtils
- .createSmallSet();
+ private final Set<Listener> mListeners = new ArraySet<>();
+ private final Set<TableEntriesUpdatedListener> mTableEntriesUpdatedListeners = new ArraySet<>();
+
+ private final Set<TableEntryChangedListener> mTableEntryChangedListeners = new ArraySet<>();
private final ChannelDataManager.Listener mChannelDataManagerListener =
new ChannelDataManager.Listener() {
@@ -197,12 +209,49 @@ public class ProgramManager {
}
};
+ private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener =
+ new DvrDataManager.ScheduledRecordingListener() {
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording scheduledRecording) {
+ TableEntry oldEntry = getTableEntry(scheduledRecording);
+ if (oldEntry != null) {
+ TableEntry newEntry = new TableEntry(oldEntry.channelId, oldEntry.program,
+ scheduledRecording, oldEntry.entryStartUtcMillis,
+ oldEntry.entryEndUtcMillis, oldEntry.isBlocked());
+ updateEntry(oldEntry, newEntry);
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording scheduledRecording) {
+ TableEntry oldEntry = getTableEntry(scheduledRecording);
+ if (oldEntry != null) {
+ TableEntry newEntry = new TableEntry(oldEntry.channelId, oldEntry.program, null,
+ oldEntry.entryStartUtcMillis, oldEntry.entryEndUtcMillis,
+ oldEntry.isBlocked());
+ updateEntry(oldEntry, newEntry);
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording scheduledRecording) {
+ TableEntry oldEntry = getTableEntry(scheduledRecording);
+ if (oldEntry != null) {
+ TableEntry newEntry = new TableEntry(oldEntry.channelId, oldEntry.program,
+ scheduledRecording, oldEntry.entryStartUtcMillis,
+ oldEntry.entryEndUtcMillis, oldEntry.isBlocked());
+ updateEntry(oldEntry, newEntry);
+ }
+ }
+ };
+
public ProgramManager(TvInputManagerHelper tvInputManagerHelper,
- ChannelDataManager channelDataManager,
- ProgramDataManager programDataManager) {
+ ChannelDataManager channelDataManager, ProgramDataManager programDataManager,
+ @Nullable DvrDataManager dvrDataManager) {
mTvInputManagerHelper = tvInputManagerHelper;
mChannelDataManager = channelDataManager;
mProgramDataManager = programDataManager;
+ mDvrDataManager = dvrDataManager;
}
public void programGuideVisibilityChanged(boolean visible) {
@@ -210,41 +259,61 @@ public class ProgramManager {
if (visible) {
mChannelDataManager.addListener(mChannelDataManagerListener);
mProgramDataManager.addListener(mProgramDataManagerListener);
+ if (mDvrDataManager != null) {
+ mDvrDataManager.addScheduledRecordingListener(mScheduledRecordingListener);
+ }
} else {
mChannelDataManager.removeListener(mChannelDataManagerListener);
mProgramDataManager.removeListener(mProgramDataManagerListener);
+ if (mDvrDataManager != null) {
+ mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener);
+ }
}
}
/**
- * Add a {@link Listener}.
+ * Adds a {@link Listener}.
*/
public void addListener(Listener listener) {
mListeners.add(listener);
}
/**
- * Register a listener to be invoked when table entries are updated.
+ * Registers a listener to be invoked when table entries are updated.
*/
public void addTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) {
mTableEntriesUpdatedListeners.add(listener);
}
/**
- * Remove a {@link Listener}.
+ * Registers a listener to be invoked when a table entry is changed.
+ */
+ public void addTableEntryChangedListener(TableEntryChangedListener listener) {
+ mTableEntryChangedListeners.add(listener);
+ }
+
+ /**
+ * Removes a {@link Listener}.
*/
public void removeListener(Listener listener) {
mListeners.remove(listener);
}
/**
- * Remove a previously installed table entries update listener.
+ * Removes a previously installed table entries update listener.
*/
public void removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) {
mTableEntriesUpdatedListeners.remove(listener);
}
/**
+ * Removes a previously installed table entry changed listener.
+ */
+ public void removeTableEntryChangedListener(TableEntryChangedListener listener) {
+ mTableEntryChangedListeners.remove(listener);
+ }
+
+ /**
* Build genre filters based on the current programs.
* This categories channels by its current program's canonical genres
* and subsequent @{link resetChannelListWithGenre(int)} calls will reset channel list
@@ -366,6 +435,7 @@ public class ProgramManager {
} else if (lastEntry.entryEndUtcMillis == Long.MAX_VALUE) {
entries.remove(entries.size() - 1);
entries.add(new TableEntry(lastEntry.channelId, lastEntry.program,
+ lastEntry.scheduledRecording,
lastEntry.entryStartUtcMillis, mEndUtcMillis,
lastEntry.mIsBlocked));
}
@@ -403,6 +473,37 @@ public class ProgramManager {
}
}
+ private void notifyTableEntryUpdated(TableEntry entry) {
+ for (TableEntryChangedListener listener : mTableEntryChangedListeners) {
+ listener.onTableEntryChanged(entry);
+ }
+ }
+
+ private void updateEntry(TableEntry old, TableEntry newEntry) {
+ List<TableEntry> entries = mChannelIdEntriesMap.get(old.channelId);
+ int index = entries.indexOf(old);
+ entries.set(index, newEntry);
+ notifyTableEntryUpdated(newEntry);
+ }
+
+ @Nullable
+ private TableEntry getTableEntry(ScheduledRecording scheduledRecording) {
+ return getTableEntry(scheduledRecording.getChannelId(), scheduledRecording.getProgramId());
+ }
+
+ @Nullable
+ private TableEntry getTableEntry(long channelId, long entryId) {
+ List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
+ if (entries != null) {
+ for (TableEntry entry : entries) {
+ if (entry.getId() == entryId) {
+ return entry;
+ }
+ }
+ }
+ return null;
+ }
+
/**
* Returns the start time of currently managed time range, in UTC millisecond.
*/
@@ -471,6 +572,14 @@ public class ProgramManager {
}
/**
+ * Returns the index of channel with {@code channelId} within the currently managed channels.
+ * Returns -1 if such a channel is not found.
+ */
+ public int getChannelIndex(long channelId) {
+ return getChannelIndex(mChannelDataManager.getChannel(channelId));
+ }
+
+ /**
* Returns the number of "entries", which lies within the currently managed time range, for a
* given {@code channelId}.
*/
@@ -511,8 +620,10 @@ public class ProgramManager {
lastProgramEndTime = programStartTime;
}
if (programEndTime > lastProgramEndTime) {
- entries.add(new TableEntry(channelId, program, lastProgramEndTime,
- programEndTime));
+ ScheduledRecording scheduledRecording = mDvrDataManager == null ? null
+ : mDvrDataManager.getScheduledRecordingForProgramId(program.getId());
+ entries.add(new TableEntry(channelId, program, scheduledRecording,
+ lastProgramEndTime, programEndTime, false));
lastProgramEndTime = programEndTime;
}
}
@@ -525,7 +636,8 @@ public class ProgramManager {
// the first entry from UI perspective. So we clip it out.
entries.remove(0);
entries.set(0, new TableEntry(secondEntry.channelId, secondEntry.program,
- mStartUtcMillis, secondEntry.entryEndUtcMillis));
+ secondEntry.scheduledRecording, mStartUtcMillis,
+ secondEntry.entryEndUtcMillis, secondEntry.mIsBlocked));
}
}
return entries;
@@ -555,6 +667,10 @@ public class ProgramManager {
void onTableEntriesUpdated();
}
+ public interface TableEntryChangedListener {
+ void onTableEntryChanged(TableEntry entry);
+ }
+
public static class ListenerAdapter implements Listener {
@Override
public void onGenresUpdated() { }
@@ -598,9 +714,24 @@ public class ProgramManager {
}
/**
- * Returns the program index of the program at {@code time}.
+ * Returns the program index of the program with {@code entryId} or -1 if not found.
+ */
+ public int getProgramIdIndex(long channelId, long entryId) {
+ List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
+ if (entries != null) {
+ for (int i = 0; i < entries.size(); i++) {
+ if (entries.get(i).getId() == entryId) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the program index of the program at {@code time} or -1 if not found.
*/
- public int getProgramIndex(long channelId, long time) {
+ public int getProgramIndexAtTime(long channelId, long time) {
List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
for (int i = 0; i < entries.size(); ++i) {
TableEntry entry = entries.get(i);
diff --git a/src/com/android/tv/guide/ProgramRow.java b/src/com/android/tv/guide/ProgramRow.java
index 4f38b879..54b864db 100644
--- a/src/com/android/tv/guide/ProgramRow.java
+++ b/src/com/android/tv/guide/ProgramRow.java
@@ -306,7 +306,7 @@ public class ProgramRow extends TimelineGridView {
public void resetScroll(int scrollOffset) {
long startTime = GuideUtils.convertPixelToMillis(scrollOffset)
+ mProgramManager.getStartTime();
- int position = mChannel == null ? -1 : mProgramManager.getProgramIndex(
+ int position = mChannel == null ? -1 : mProgramManager.getProgramIndexAtTime(
mChannel.getId(), startTime);
if (position < 0) {
getLayoutManager().scrollToPosition(0);
diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java
index a86c1332..83755b5f 100644
--- a/src/com/android/tv/guide/ProgramTableAdapter.java
+++ b/src/com/android/tv/guide/ProgramTableAdapter.java
@@ -26,7 +26,9 @@ import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.media.tv.TvContentRating;
+import android.media.tv.TvInputInfo;
import android.os.Handler;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.RecycledViewPool;
@@ -43,11 +45,15 @@ import android.widget.ImageView;
import android.widget.TextView;
import com.android.tv.R;
+import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.guide.ProgramManager.TableEntriesUpdatedListener;
import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter;
+import com.android.tv.util.ImageCache;
+import com.android.tv.util.ImageLoader;
+import com.android.tv.util.ImageLoader.LoadTvInputLogoTask;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -57,8 +63,8 @@ import java.util.List;
/**
* Adapts the {@link ProgramListAdapter} list to the body of the program guide table.
*/
-public class ProgramTableAdapter extends
- RecyclerView.Adapter<ProgramTableAdapter.ProgramRowHolder> {
+public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.ProgramRowHolder>
+ implements ProgramManager.TableEntryChangedListener {
private static final String TAG = "ProgramTableAdapter";
private static final boolean DEBUG = false;
@@ -84,10 +90,10 @@ public class ProgramTableAdapter extends
private final int mDetailPadding;
private final TextAppearanceSpan mEpisodeTitleStyle;
- public ProgramTableAdapter(Context context, TvInputManagerHelper tvInputManagerHelper,
- ProgramManager programManager, ProgramGuide programGuide) {
+ public ProgramTableAdapter(Context context, ProgramManager programManager,
+ ProgramGuide programGuide) {
mContext = context;
- mTvInputManagerHelper = tvInputManagerHelper;
+ mTvInputManagerHelper = TvApplication.getSingletons(context).getTvInputManagerHelper();
mProgramManager = programManager;
mProgramGuide = programGuide;
@@ -140,6 +146,7 @@ public class ProgramTableAdapter extends
}
});
update();
+ mProgramManager.addTableEntryChangedListener(this);
}
private void update() {
@@ -149,7 +156,8 @@ public class ProgramTableAdapter extends
}
mProgramListAdapters.clear();
for (int i = 0; i < mProgramManager.getChannelCount(); i++) {
- ProgramListAdapter listAdapter = new ProgramListAdapter(mContext, mProgramManager, i);
+ ProgramListAdapter listAdapter = new ProgramListAdapter(mContext.getResources(),
+ mProgramManager, i);
mProgramManager.addTableEntriesUpdatedListener(listAdapter);
mProgramListAdapters.add(listAdapter);
}
@@ -179,6 +187,14 @@ public class ProgramTableAdapter extends
return new ProgramRowHolder(itemView);
}
+ @Override
+ public void onTableEntryChanged(ProgramManager.TableEntry tableEntry) {
+ int channelIndex = mProgramManager.getChannelIndex(tableEntry.channelId);
+ int pos = mProgramManager.getProgramIdIndex(tableEntry.channelId, tableEntry.getId());
+ if (DEBUG) Log.d(TAG, "update(" + channelIndex + ", " + pos + ")");
+ mProgramListAdapters.get(channelIndex).notifyItemChanged(pos, tableEntry);
+ }
+
// TODO: make it static
public class ProgramRowHolder extends RecyclerView.ViewHolder
implements ProgramRow.ChildFocusListener {
@@ -223,6 +239,9 @@ public class ProgramTableAdapter extends
private final TextView mChannelNameView;
private final ImageView mChannelLogoView;
private final ImageView mChannelBlockView;
+ private final ImageView mInputLogoView;
+
+ private boolean mIsInputLogoVisible;
public ProgramRowHolder(View itemView) {
super(itemView);
@@ -244,6 +263,7 @@ public class ProgramTableAdapter extends
mChannelNameView = (TextView) mContainer.findViewById(R.id.channel_name);
mChannelLogoView = (ImageView) mContainer.findViewById(R.id.channel_logo);
mChannelBlockView = (ImageView) mContainer.findViewById(R.id.channel_block);
+ mInputLogoView = (ImageView) mContainer.findViewById(R.id.input_logo);
}
public void onBind(int position) {
@@ -267,6 +287,8 @@ public class ProgramTableAdapter extends
if (DEBUG) Log.d(TAG, "onBindChannel " + channel);
mChannel = channel;
+ mInputLogoView.setVisibility(View.GONE);
+ mIsInputLogoVisible = false;
if (channel == null) {
mChannelNumberView.setVisibility(View.GONE);
mChannelNameView.setVisibility(View.GONE);
@@ -467,6 +489,43 @@ public class ProgramTableAdapter extends
}
}
+ /**
+ * Update tv input logo. It should be called when the visible child item in ProgramGrid
+ * changed.
+ */
+ public void updateInputLogo(int lastPosition, boolean forceShow) {
+ if (mChannel == null) {
+ mInputLogoView.setVisibility(View.GONE);
+ mIsInputLogoVisible = false;
+ return;
+ }
+
+ boolean showLogo = forceShow;
+ if (!showLogo) {
+ Channel lastChannel = mProgramManager.getChannel(lastPosition);
+ if (lastChannel == null
+ || !mChannel.getInputId().equals(lastChannel.getInputId())) {
+ showLogo = true;
+ }
+ }
+
+ if (showLogo) {
+ if (!mIsInputLogoVisible) {
+ mIsInputLogoVisible = true;
+ TvInputInfo info = mTvInputManagerHelper.getTvInputInfo(mChannel.getInputId());
+ if (info != null) {
+ LoadTvInputLogoTask task = new LoadTvInputLogoTask(
+ itemView.getContext(), ImageCache.getInstance(), info);
+ ImageLoader.loadBitmap(createTvInputLogoLoadedCallback(info, this), task);
+ }
+ }
+ } else {
+ mInputLogoView.setVisibility(View.GONE);
+ mInputLogoView.setImageDrawable(null);
+ mIsInputLogoVisible = false;
+ }
+ }
+
private void updateTextView(TextView textView, String text) {
if (!TextUtils.isEmpty(text)) {
textView.setVisibility(View.VISIBLE);
@@ -487,6 +546,14 @@ public class ProgramTableAdapter extends
mChannelLogoView.setVisibility(View.VISIBLE);
}
+ private void updateInputLogoInternal(@NonNull Bitmap tvInputLogo) {
+ if (!mIsInputLogoVisible) {
+ return;
+ }
+ mInputLogoView.setImageBitmap(tvInputLogo);
+ mInputLogoView.setVisibility(View.VISIBLE);
+ }
+
private void onHorizontalScrolled() {
if (mDetailInAnimator != null) {
mHandler.removeCallbacks(mDetailInStarter);
@@ -526,4 +593,17 @@ public class ProgramTableAdapter extends
}
};
}
+
+ private static ImageLoaderCallback<ProgramRowHolder> createTvInputLogoLoadedCallback(
+ final TvInputInfo info, ProgramRowHolder holder) {
+ return new ImageLoaderCallback<ProgramRowHolder>(holder) {
+ @Override
+ public void onBitmapLoaded(ProgramRowHolder holder, @Nullable Bitmap logo) {
+ if (logo != null && info.getId()
+ .equals(holder.mChannel.getInputId())) {
+ holder.updateInputLogoInternal(logo);
+ }
+ }
+ };
+ }
}
diff --git a/src/com/android/tv/guide/TimelineRow.java b/src/com/android/tv/guide/TimelineRow.java
index 891b14cd..3f0c8678 100644
--- a/src/com/android/tv/guide/TimelineRow.java
+++ b/src/com/android/tv/guide/TimelineRow.java
@@ -64,7 +64,9 @@ public class TimelineRow extends TimelineGridView {
public void onRtlPropertiesChanged(int layoutDirection) {
super.onRtlPropertiesChanged(layoutDirection);
// Reset scroll
- scrollTo(getScrollOffset(), false);
+ if (isAttachedToWindow()) {
+ scrollTo(getScrollOffset(), false);
+ }
}
@Override
diff --git a/src/com/android/tv/menu/ActionCardView.java b/src/com/android/tv/menu/ActionCardView.java
index 1848a3ce..2d72b06f 100644
--- a/src/com/android/tv/menu/ActionCardView.java
+++ b/src/com/android/tv/menu/ActionCardView.java
@@ -93,4 +93,7 @@ public class ActionCardView extends FrameLayout implements ItemListRowView.CardV
Log.d(TAG, "onDeselected: action=" + mLabelView.getText());
}
}
+
+ @Override
+ public void onRecycled() { }
}
diff --git a/src/com/android/tv/menu/BaseCardView.java b/src/com/android/tv/menu/BaseCardView.java
index 25d4e313..b4500dd1 100644
--- a/src/com/android/tv/menu/BaseCardView.java
+++ b/src/com/android/tv/menu/BaseCardView.java
@@ -85,6 +85,9 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
}
@Override
+ public void onRecycled() { }
+
+ @Override
public void onSelected() {
if (isAttachedToWindow() && getVisibility() == View.VISIBLE) {
startFocusAnimation(SCALE_FACTOR_1F);
diff --git a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java
index 1e416e5b..f932d75d 100644
--- a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java
+++ b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java
@@ -25,11 +25,11 @@ import android.support.annotation.NonNull;
import android.util.Log;
import com.android.tv.R;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.WeakHandler;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.data.ProgramDataManager;
-import com.android.tv.util.SoftPreconditions;
import java.util.List;
diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java
index 51867d0b..200f4ac0 100644
--- a/src/com/android/tv/menu/ChannelsRowAdapter.java
+++ b/src/com/android/tv/menu/ChannelsRowAdapter.java
@@ -18,7 +18,9 @@ package com.android.tv.menu;
import android.content.Context;
import android.content.Intent;
+import android.media.tv.TvInputInfo;
import android.os.Build;
+import android.support.v4.os.BuildCompat;
import android.view.View;
import com.android.tv.MainActivity;
@@ -29,6 +31,8 @@ import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
import com.android.tv.recommendation.Recommender;
import com.android.tv.util.SetupUtils;
+import com.android.tv.util.TvInputManagerHelper;
+import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.List;
@@ -37,16 +41,16 @@ import java.util.List;
* An adapter of the Channels row.
*/
public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel> {
- // There are four special cards: guide, setup, dvr, applink.
- private static final int SIZE_OF_VIEW_TYPE = 4;
+ // There are four special cards: guide, setup, dvr, record, applink.
+ private static final int SIZE_OF_VIEW_TYPE = 5;
private final Context mContext;
private final Tracker mTracker;
private final Recommender mRecommender;
private final int mMaxCount;
private final int mMinCount;
- private boolean mShowDvrCard;
- private int[] mViewType = new int[SIZE_OF_VIEW_TYPE];
+ private final boolean mDvrFeatureEnabled;
+ private final int[] mViewType = new int[SIZE_OF_VIEW_TYPE];
private final View.OnClickListener mGuideOnClickListener = new View.OnClickListener() {
@Override
@@ -67,11 +71,28 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
private final View.OnClickListener mDvrOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
+ Utils.showToastMessageForDeveloperFeature(view.getContext());
mTracker.sendMenuClicked(R.string.channels_item_dvr);
getMainActivity().getOverlayManager().showDvrManager();
}
};
+ private final View.OnClickListener mRecordOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Utils.showToastMessageForDeveloperFeature(view.getContext());
+ RecordCardView v = ((RecordCardView) view);
+ boolean isRecording = v.isRecording();
+ mTracker.sendMenuClicked(isRecording ? R.string.channels_item_record_start
+ : R.string.channels_item_record_stop);
+ if (!isRecording) {
+ v.startRecording();
+ } else {
+ v.stopRecording();
+ }
+ }
+ };
+
private final View.OnClickListener mAppLinkOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
@@ -102,7 +123,7 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
mRecommender = recommender;
mMinCount = minCount;
mMaxCount = maxCount;
- mShowDvrCard = CommonFeatures.DVR.isEnabled(mContext);
+ mDvrFeatureEnabled = CommonFeatures.DVR.isEnabled(mContext) && BuildCompat.isAtLeastN();
}
@Override
@@ -131,6 +152,8 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
viewHolder.itemView.setOnClickListener(mAppLinkOnClickListener);
} else if (viewType == R.layout.menu_card_dvr) {
viewHolder.itemView.setOnClickListener(mDvrOnClickListener);
+ } else if (viewType == R.layout.menu_card_record) {
+ viewHolder.itemView.setOnClickListener(mRecordOnClickListener);
} else {
viewHolder.itemView.setTag(getItemList().get(position));
viewHolder.itemView.setOnClickListener(mChannelOnClickListener);
@@ -140,17 +163,33 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
@Override
public void update() {
List<Channel> channelList = new ArrayList<>();
- Channel dummyChannel = new Channel.Builder()
- .build();
+ Channel dummyChannel = new Channel.Builder().build();
// For guide item
channelList.add(dummyChannel);
// For setup item
- boolean showSetupCard = SetupUtils.getInstance(mContext)
- .hasNewInput(((MainActivity) mContext).getTvInputManagerHelper());
+ TvInputManagerHelper inputManager = TvApplication.getSingletons(mContext)
+ .getTvInputManagerHelper();
+ boolean showSetupCard = SetupUtils.getInstance(mContext).hasNewInput(inputManager);
Channel currentChannel = ((MainActivity) mContext).getCurrentChannel();
boolean showAppLinkCard = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& currentChannel != null
&& currentChannel.getAppLinkType(mContext) != Channel.APP_LINK_TYPE_NONE;
+ boolean showDvrCard = false;
+ boolean showRecordCard = false;
+ if (mDvrFeatureEnabled) {
+ for (TvInputInfo info : inputManager.getTvInputInfos(true, true)) {
+ if (info.canRecord()) {
+ showDvrCard = true;
+ break;
+ }
+ }
+ if (currentChannel != null && currentChannel.getInputId() != null) {
+ TvInputInfo inputInfo = inputManager.getTvInputInfo(currentChannel.getInputId());
+ if ((inputInfo.canRecord() && inputInfo.getTunerCount() > 1)) {
+ showRecordCard = true;
+ }
+ }
+ }
mViewType[0] = R.layout.menu_card_guide;
int index = 1;
@@ -158,10 +197,14 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
channelList.add(dummyChannel);
mViewType[index++] = R.layout.menu_card_setup;
}
- if (mShowDvrCard) {
+ if (showDvrCard) {
channelList.add(dummyChannel);
mViewType[index++] = R.layout.menu_card_dvr;
}
+ if (showRecordCard) {
+ channelList.add(currentChannel);
+ mViewType[index++] = R.layout.menu_card_record;
+ }
if (showAppLinkCard) {
channelList.add(currentChannel);
mViewType[index++] = R.layout.menu_card_app_link;
diff --git a/src/com/android/tv/menu/ItemListRowView.java b/src/com/android/tv/menu/ItemListRowView.java
index e9362a78..4919c595 100644
--- a/src/com/android/tv/menu/ItemListRowView.java
+++ b/src/com/android/tv/menu/ItemListRowView.java
@@ -41,6 +41,7 @@ public class ItemListRowView extends MenuRowView implements OnChildSelectedListe
public interface CardView<T> {
void onBind(T row, boolean selected);
+ void onRecycled();
void onSelected();
void onDeselected();
}
@@ -206,6 +207,13 @@ public class ItemListRowView extends MenuRowView implements OnChildSelectedListe
cardView.onBind(mItemList.get(position), cardView.equals(mItemListView.mSelectedCard));
}
+ @Override
+ public void onViewRecycled(MyViewHolder viewHolder) {
+ super.onViewRecycled(viewHolder);
+ CardView<T> cardView = (CardView<T>) viewHolder.itemView;
+ cardView.onRecycled();
+ }
+
public static class MyViewHolder extends RecyclerView.ViewHolder {
public MyViewHolder(View view) {
super(view);
diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java
index 613e0d62..7bb0787e 100644
--- a/src/com/android/tv/menu/Menu.java
+++ b/src/com/android/tv/menu/Menu.java
@@ -56,7 +56,7 @@ public class Menu {
@IntDef({REASON_NONE, REASON_GUIDE, REASON_PLAY_CONTROLS_PLAY, REASON_PLAY_CONTROLS_PAUSE,
REASON_PLAY_CONTROLS_PLAY_PAUSE, REASON_PLAY_CONTROLS_REWIND,
REASON_PLAY_CONTROLS_FAST_FORWARD, REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS,
- REASON_PLAY_CONTROLS_JUMP_TO_NEXT})
+ REASON_PLAY_CONTROLS_JUMP_TO_NEXT, REASON_RECORDING_PLAYBACK})
public @interface MenuShowReason {}
public static final int REASON_NONE = 0;
public static final int REASON_GUIDE = 1;
@@ -67,6 +67,7 @@ public class Menu {
public static final int REASON_PLAY_CONTROLS_FAST_FORWARD = 6;
public static final int REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS = 7;
public static final int REASON_PLAY_CONTROLS_JUMP_TO_NEXT = 8;
+ public static final int REASON_RECORDING_PLAYBACK = 9;
private static final List<String> sRowIdListForReason = new ArrayList<>();
static {
@@ -79,6 +80,7 @@ public class Menu {
sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_FAST_FORWARD
sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS
sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_JUMP_TO_NEXT
+ sRowIdListForReason.add(PlayControlsRow.ID); // REASON_RECORDING_PLAYBACK
}
private static final String SCREEN_NAME = "Menu";
diff --git a/src/com/android/tv/menu/MenuAction.java b/src/com/android/tv/menu/MenuAction.java
index b45e88c2..86153084 100644
--- a/src/com/android/tv/menu/MenuAction.java
+++ b/src/com/android/tv/menu/MenuAction.java
@@ -36,8 +36,11 @@ public class MenuAction {
public static final MenuAction SELECT_DISPLAY_MODE_ACTION =
new MenuAction(R.string.options_item_display_mode, TvOptionsManager.OPTION_DISPLAY_MODE,
R.drawable.ic_tvoption_aspect);
- public static final MenuAction PIP_ACTION =
- new MenuAction(R.string.options_item_pip, TvOptionsManager.OPTION_PIP,
+ public static final MenuAction PIP_IN_APP_ACTION =
+ new MenuAction(R.string.options_item_pip, TvOptionsManager.OPTION_IN_APP_PIP,
+ R.drawable.ic_tvoption_pip);
+ public static final MenuAction SYSTEMWIDE_PIP_ACTION =
+ new MenuAction(R.string.options_item_pip, TvOptionsManager.OPTION_SYSTEMWIDE_PIP,
R.drawable.ic_tvoption_pip);
public static final MenuAction SELECT_AUDIO_LANGUAGE_ACTION =
new MenuAction(R.string.options_item_multi_audio, TvOptionsManager.OPTION_MULTI_AUDIO,
diff --git a/src/com/android/tv/menu/MenuLayoutManager.java b/src/com/android/tv/menu/MenuLayoutManager.java
index 265ad840..1f377f54 100644
--- a/src/com/android/tv/menu/MenuLayoutManager.java
+++ b/src/com/android/tv/menu/MenuLayoutManager.java
@@ -35,7 +35,7 @@ import android.view.ViewGroup.MarginLayoutParams;
import android.widget.TextView;
import com.android.tv.R;
-import com.android.tv.util.SoftPreconditions;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.util.Utils;
import java.util.ArrayList;
@@ -318,6 +318,11 @@ public class MenuLayoutManager {
if (!indexValid) {
return;
}
+ MenuRow row = mMenuRows.get(position);
+ if (!row.isVisible()) {
+ Log.e(TAG, "Selecting invisible row: " + position);
+ return;
+ }
if (Utils.isIndexValid(mMenuRowViews, mSelectedPosition)) {
mMenuRowViews.get(mSelectedPosition).onDeselected();
}
@@ -360,6 +365,11 @@ public class MenuLayoutManager {
if (!newIndexValid) {
return;
}
+ MenuRow row = mMenuRows.get(position);
+ if (!row.isVisible()) {
+ Log.e(TAG, "Moving to the invisible row: " + position);
+ return;
+ }
if (mAnimatorSet != null) {
// Do not cancel the animation here. The property values should be set to the end values
// when the animation finishes.
@@ -787,9 +797,9 @@ public class MenuLayoutManager {
}
private static final class ViewPropertyValueHolder {
- public Property<View, Float> property;
- public View view;
- public float value;
+ public final Property<View, Float> property;
+ public final View view;
+ public final float value;
public ViewPropertyValueHolder(Property<View, Float> property, View view, float value) {
this.property = property;
diff --git a/src/com/android/tv/menu/MenuView.java b/src/com/android/tv/menu/MenuView.java
index df91ddf3..e012dfca 100644
--- a/src/com/android/tv/menu/MenuView.java
+++ b/src/com/android/tv/menu/MenuView.java
@@ -117,10 +117,11 @@ public class MenuView extends FrameLayout implements IMenuView {
}
initializeChildren();
update(true);
- if (rowIdToSelect == null) {
- rowIdToSelect = ChannelsRow.ID;
- }
int position = getItemPosition(rowIdToSelect);
+ if (position == -1 || !mMenuRows.get(position).isVisible()) {
+ // Channels row is always visible.
+ position = getItemPosition(ChannelsRow.ID);
+ }
setSelectedPosition(position);
// Change the visibility as late as possible to avoid the unnecessary animation.
setVisibility(VISIBLE);
diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java
index f0853c40..058d5108 100644
--- a/src/com/android/tv/menu/PlayControlsRowView.java
+++ b/src/com/android/tv/menu/PlayControlsRowView.java
@@ -19,6 +19,7 @@ package com.android.tv.menu;
import android.content.Context;
import android.content.res.Resources;
import android.text.format.DateFormat;
+import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
@@ -27,6 +28,7 @@ import android.widget.TextView;
import com.android.tv.R;
import com.android.tv.TimeShiftManager;
import com.android.tv.TimeShiftManager.TimeShiftActionId;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Program;
import com.android.tv.menu.Menu.MenuShowReason;
@@ -249,9 +251,16 @@ public class PlayControlsRowView extends MenuRowView {
}
private void initializeTimeline() {
- Program program = mTimeShiftManager.getProgramAt(mTimeShiftManager.getCurrentPositionMs());
- mProgramStartTimeMs = program.getStartTimeUtcMillis();
- mProgramEndTimeMs = program.getEndTimeUtcMillis();
+ if (mTimeShiftManager.isRecordingPlayback()) {
+ mProgramStartTimeMs = mTimeShiftManager.getRecordStartTimeMs();
+ mProgramEndTimeMs = mTimeShiftManager.getRecordEndTimeMs();
+ } else {
+ Program program = mTimeShiftManager.getProgramAt(
+ mTimeShiftManager.getCurrentPositionMs());
+ mProgramStartTimeMs = program.getStartTimeUtcMillis();
+ mProgramEndTimeMs = program.getEndTimeUtcMillis();
+ }
+ SoftPreconditions.checkArgument(mProgramStartTimeMs <= mProgramEndTimeMs);
}
private void updateMenuVisibility() {
@@ -357,14 +366,6 @@ public class PlayControlsRowView extends MenuRowView {
mTimeIndicator.setVisibility(View.INVISIBLE);
return;
}
- if (mTimeShiftManager.isPlayForRecording()) {
- mProgramStartTimeMs = mTimeShiftManager.getRecordStartTimeMs();
- mProgramEndTimeMs = Math.max(mProgramStartTimeMs,
- mTimeShiftManager.getRecordEndTimeMs());
- if (mProgramStartTimeMs > mProgramEndTimeMs) {
- mProgramEndTimeMs = mProgramStartTimeMs;
- }
- }
long currentPositionMs = mTimeShiftManager.getCurrentPositionMs();
ViewGroup.MarginLayoutParams params =
(ViewGroup.MarginLayoutParams) mTimeText.getLayoutParams();
@@ -422,15 +423,18 @@ public class PlayControlsRowView extends MenuRowView {
private void updateRecTimeText() {
if (isEnabled()) {
- mProgramStartTimeText.setVisibility(View.VISIBLE);
+ if (mTimeShiftManager.isRecordingPlayback()) {
+ mProgramStartTimeText.setVisibility(View.GONE);
+ } else {
+ mProgramStartTimeText.setVisibility(View.VISIBLE);
+ mProgramStartTimeText.setText(getTimeString(mProgramStartTimeMs));
+ }
mProgramEndTimeText.setVisibility(View.VISIBLE);
+ mProgramEndTimeText.setText(getTimeString(mProgramEndTimeMs));
} else {
- mProgramStartTimeText.setVisibility(View.INVISIBLE);
- mProgramEndTimeText.setVisibility(View.INVISIBLE);
- return;
+ mProgramStartTimeText.setVisibility(View.GONE);
+ mProgramEndTimeText.setVisibility(View.GONE);
}
- mProgramStartTimeText.setText(getTimeString(mProgramStartTimeMs));
- mProgramEndTimeText.setText(getTimeString(mProgramEndTimeMs));
}
private void updateButtons() {
@@ -478,7 +482,9 @@ public class PlayControlsRowView extends MenuRowView {
}
private String getTimeString(long timeMs) {
- return mTimeFormat.format(timeMs);
+ return mTimeShiftManager.isRecordingPlayback()
+ ? DateUtils.formatElapsedTime(timeMs / 1000)
+ : mTimeFormat.format(timeMs);
}
private int convertDurationToPixel(long duration) {
diff --git a/src/com/android/tv/menu/RecordCardView.java b/src/com/android/tv/menu/RecordCardView.java
new file mode 100644
index 00000000..de30894e
--- /dev/null
+++ b/src/com/android/tv/menu/RecordCardView.java
@@ -0,0 +1,189 @@
+/*
+ * 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.menu;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.tv.MainActivity;
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.data.Channel;
+import com.android.tv.data.Program;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.ScheduledRecording;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A view to render an item of TV options.
+ */
+public class RecordCardView extends SimpleCardView implements
+ DvrDataManager.ScheduledRecordingListener {
+ private static final String TAG = MenuView.TAG;
+ private static final boolean DEBUG = MenuView.DEBUG;
+ private static final long MIN_PROGRAM_RECORD_DURATION = TimeUnit.MINUTES.toMillis(5);
+
+ private ImageView mIconView;
+ private TextView mLabelView;
+ private Channel mCurrentChannel;
+ private final DvrManager mDvrManager;
+ private final DvrDataManager mDvrDataManager;
+ private boolean mIsRecording;
+ private ScheduledRecording mCurrentRecording;
+
+ public RecordCardView(Context context) {
+ this(context, null);
+ }
+
+ public RecordCardView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public RecordCardView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mDvrManager = TvApplication.getSingletons(context).getDvrManager();
+ mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
+ }
+
+ @Override
+ public void onBind(Channel channel, boolean selected) {
+ super.onBind(channel, selected);
+ mIconView = (ImageView) findViewById(R.id.record_icon);
+ mLabelView = (TextView) findViewById(R.id.record_label);
+ mCurrentChannel = channel;
+ mCurrentRecording = null;
+ for (ScheduledRecording recording : mDvrDataManager.getStartedRecordings()) {
+ if (recording.getChannelId() == channel.getId()) {
+ mIsRecording = true;
+ mCurrentRecording = recording;
+ }
+ }
+ mDvrDataManager.addScheduledRecordingListener(this);
+ updateCardView();
+ }
+
+ @Override
+ public void onRecycled() {
+ super.onRecycled();
+ mDvrDataManager.removeScheduledRecordingListener(this);
+ }
+
+ public boolean isRecording() {
+ return mIsRecording;
+ }
+
+ public void startRecording() {
+ showStartRecordingDialog();
+ }
+
+ public void stopRecording() {
+ mDvrManager.stopRecording(mCurrentRecording);
+ }
+
+ private void updateCardView() {
+ if (mIsRecording) {
+ mIconView.setImageResource(R.drawable.ic_record_stop);
+ mLabelView.setText(R.string.channels_item_record_stop);
+ } else {
+ mIconView.setImageResource(R.drawable.ic_record_start);
+ mLabelView.setText(R.string.channels_item_record_start);
+ }
+ }
+
+ private void showStartRecordingDialog() {
+ final long endOfProgram = -1;
+
+ final List<CharSequence> items = new ArrayList<>();
+ final List<Long> durations = new ArrayList<>();
+ Resources res = getResources();
+ items.add(res.getString(R.string.recording_start_dialog_10_min_duration));
+ durations.add(TimeUnit.MINUTES.toMillis(10));
+ items.add(res.getString(R.string.recording_start_dialog_30_min_duration));
+ durations.add(TimeUnit.MINUTES.toMillis(30));
+ items.add(res.getString(R.string.recording_start_dialog_1_hour_duration));
+ durations.add(TimeUnit.HOURS.toMillis(1));
+ items.add(res.getString(R.string.recording_start_dialog_3_hours_duration));
+ durations.add(TimeUnit.HOURS.toMillis(3));
+
+ Program currenProgram = ((MainActivity) getContext()).getCurrentProgram(false);
+ if (currenProgram != null) {
+ long duration = currenProgram.getEndTimeUtcMillis() - System.currentTimeMillis();
+ if (duration > MIN_PROGRAM_RECORD_DURATION) {
+ items.add(res.getString(R.string.recording_start_dialog_till_end_of_program));
+ durations.add(duration);
+ }
+ }
+
+ DialogInterface.OnClickListener onClickListener
+ = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog, int which) {
+ long startTime = System.currentTimeMillis();
+ long endTime = System.currentTimeMillis() + durations.get(which);
+ mDvrManager.addSchedule(mCurrentChannel, startTime, endTime);
+ dialog.dismiss();
+ }
+ };
+ new AlertDialog.Builder(getContext())
+ .setItems(items.toArray(new CharSequence[items.size()]), onClickListener)
+ .create()
+ .show();
+ }
+
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording recording) {
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording recording) {
+ if (recording.getChannelId() != mCurrentChannel.getId()) {
+ return;
+ }
+ if (mIsRecording) {
+ mIsRecording = false;
+ mCurrentRecording = null;
+ updateCardView();
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording recording) {
+ if (recording.getChannelId() != mCurrentChannel.getId()) {
+ return;
+ }
+ int state = recording.getState();
+ if (state == ScheduledRecording.STATE_RECORDING_FAILED
+ || state == ScheduledRecording.STATE_RECORDING_FINISHED) {
+ mIsRecording = false;
+ mCurrentRecording = null;
+ updateCardView();
+ } else if (state == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
+ mIsRecording = true;
+ mCurrentRecording = recording;
+ updateCardView();
+ }
+ }
+}
diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java
index 82525456..ba84247b 100644
--- a/src/com/android/tv/menu/TvOptionsRowAdapter.java
+++ b/src/com/android/tv/menu/TvOptionsRowAdapter.java
@@ -19,6 +19,7 @@ package com.android.tv.menu;
import android.content.Context;
import android.media.tv.TvTrackInfo;
import android.support.annotation.VisibleForTesting;
+import android.support.v4.os.BuildCompat;
import com.android.tv.Features;
import com.android.tv.R;
@@ -39,10 +40,13 @@ import java.util.List;
*/
public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
private int mPositionPipAction;
- private boolean mHasPipAction = true;
+ // If mInAppPipAction is false, system-wide PIP is used.
+ private boolean mInAppPipAction = true;
+ private final Context mContext;
public TvOptionsRowAdapter(Context context, List<CustomAction> customActions) {
super(context, customActions);
+ mContext = context;
}
@Override
@@ -52,8 +56,8 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
setOptionChangedListener(MenuAction.SELECT_CLOSED_CAPTION_ACTION);
actionList.add(MenuAction.SELECT_DISPLAY_MODE_ACTION);
setOptionChangedListener(MenuAction.SELECT_DISPLAY_MODE_ACTION);
- actionList.add(MenuAction.PIP_ACTION);
- setOptionChangedListener(MenuAction.PIP_ACTION);
+ actionList.add(MenuAction.PIP_IN_APP_ACTION);
+ setOptionChangedListener(MenuAction.PIP_IN_APP_ACTION);
mPositionPipAction = actionList.size() - 1;
actionList.add(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION);
setOptionChangedListener(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION);
@@ -106,34 +110,39 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
// Case 1
PipInputManager pipInputManager = getMainActivity().getPipInputManager();
if (pipInputManager.getPipInputSize(false) < 2) {
- if (mHasPipAction) {
+ if (mInAppPipAction) {
removeAction(mPositionPipAction);
- mHasPipAction = false;
+ mInAppPipAction = false;
+ if (BuildCompat.isAtLeastN()) {
+ addAction(mPositionPipAction, MenuAction.SYSTEMWIDE_PIP_ACTION);
+ }
return true;
}
+ return false;
} else {
- if (!mHasPipAction) {
- addAction(mPositionPipAction, MenuAction.PIP_ACTION);
- mHasPipAction = true;
+ if (!mInAppPipAction) {
+ removeAction(mPositionPipAction);
+ addAction(mPositionPipAction, MenuAction.PIP_IN_APP_ACTION);
+ mInAppPipAction = true;
changed = true;
}
}
// Case 2
boolean isPipEnabled = getMainActivity().isPipEnabled();
- boolean oldEnabled = MenuAction.PIP_ACTION.isEnabled();
+ boolean oldEnabled = MenuAction.PIP_IN_APP_ACTION.isEnabled();
boolean newEnabled = pipInputManager.getPipInputSize(true) > 0;
if (oldEnabled != newEnabled) {
// Should not disable the item if the PIP is already turned on so that the user can
// force exit it.
if (newEnabled || !isPipEnabled) {
- MenuAction.PIP_ACTION.setEnabled(newEnabled);
+ MenuAction.PIP_IN_APP_ACTION.setEnabled(newEnabled);
changed = true;
}
}
// Case 3 & 4 - we just need to update the icon.
- MenuAction.PIP_ACTION.setDrawableResId(
+ MenuAction.PIP_IN_APP_ACTION.setDrawableResId(
isPipEnabled ? R.drawable.ic_tvoption_pip : R.drawable.ic_tvoption_pip_off);
return changed;
}
@@ -173,9 +182,12 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
getMainActivity().getOverlayManager().getSideFragmentManager().show(
new DisplayModeFragment());
break;
- case TvOptionsManager.OPTION_PIP:
+ case TvOptionsManager.OPTION_IN_APP_PIP:
getMainActivity().togglePipView();
break;
+ case TvOptionsManager.OPTION_SYSTEMWIDE_PIP:
+ getMainActivity().enterPictureInPictureMode();
+ break;
case TvOptionsManager.OPTION_MULTI_AUDIO:
getMainActivity().getOverlayManager().getSideFragmentManager().show(
new MultiAudioFragment());
diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java
index 3ae80597..0685d14b 100644
--- a/src/com/android/tv/onboarding/OnboardingActivity.java
+++ b/src/com/android/tv/onboarding/OnboardingActivity.java
@@ -73,48 +73,55 @@ public class OnboardingActivity extends SetupActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- // Make the channels of the new inputs which have been setup outside Live TV
- // browsable.
- mChannelDataManager = TvApplication.getSingletons(this).getChannelDataManager();
- if (mChannelDataManager.isDbLoadFinished()) {
- SetupUtils.getInstance(this).markNewChannelsBrowsable();
- } else {
- mChannelDataManager.addListener(mChannelListener);
+ if (!PermissionUtils.hasAccessAllEpg(this)) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ Toast.makeText(this, R.string.msg_not_supported_device, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ } else if (checkSelfPermission(PERMISSION_READ_TV_LISTINGS)
+ != PackageManager.PERMISSION_GRANTED) {
+ requestPermissions(new String[]{PERMISSION_READ_TV_LISTINGS},
+ PERMISSIONS_REQUEST_READ_TV_LISTINGS);
+ }
}
}
@Override
protected void onDestroy() {
- mChannelDataManager.removeListener(mChannelListener);
+ if (mChannelDataManager != null) {
+ mChannelDataManager.removeListener(mChannelListener);
+ }
super.onDestroy();
}
@Override
protected Fragment onCreateInitialFragment() {
- return OnboardingUtils.isFirstRunWithCurrentVersion(this) ? new WelcomeFragment()
- : new SetupSourcesFragment();
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- if (!PermissionUtils.hasAccessAllEpg(this)) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- Toast.makeText(this, R.string.msg_not_supported_device, Toast.LENGTH_LONG).show();
- finish();
- } else if (checkSelfPermission(PERMISSION_READ_TV_LISTINGS)
- != PackageManager.PERMISSION_GRANTED) {
- requestPermissions(new String[]{PERMISSION_READ_TV_LISTINGS},
- PERMISSIONS_REQUEST_READ_TV_LISTINGS);
+ if (PermissionUtils.hasAccessAllEpg(this) || PermissionUtils.hasReadTvListings(this)) {
+ // Make the channels of the new inputs which have been setup outside Live TV
+ // browsable.
+ mChannelDataManager = TvApplication.getSingletons(this).getChannelDataManager();
+ if (mChannelDataManager.isDbLoadFinished()) {
+ SetupUtils.getInstance(this).markNewChannelsBrowsable();
+ } else {
+ mChannelDataManager.addListener(mChannelListener);
}
+ return OnboardingUtils.isFirstRunWithCurrentVersion(this) ? new WelcomeFragment()
+ : new SetupSourcesFragment();
}
+ return null;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode == PERMISSIONS_REQUEST_READ_TV_LISTINGS) {
- if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
+ if (grantResults != null && grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ finish();
+ Intent intentForNextActivity = getIntent().getParcelableExtra(
+ KEY_INTENT_AFTER_COMPLETION);
+ startActivity(buildIntent(this, intentForNextActivity));
+ } else {
Toast.makeText(this, R.string.msg_read_tv_listing_permission_denied,
Toast.LENGTH_LONG).show();
finish();
diff --git a/src/com/android/tv/onboarding/SetupSourcesFragment.java b/src/com/android/tv/onboarding/SetupSourcesFragment.java
index ebf32d00..23145503 100644
--- a/src/com/android/tv/onboarding/SetupSourcesFragment.java
+++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java
@@ -30,7 +30,6 @@ import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
import android.support.v17.leanback.widget.GuidedActionsStylist;
import android.support.v17.leanback.widget.VerticalGridView;
-import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -60,16 +59,14 @@ import java.util.List;
* A fragment for channel source info/setup.
*/
public class SetupSourcesFragment extends SetupMultiPaneFragment {
+ private static final String TAG = "SetupSourcesFragment";
+
public static final String ACTION_CATEGORY =
"com.android.tv.onboarding.SetupSourcesFragment";
public static final int ACTION_PLAY_STORE = 1;
- public static final int DEFAULT_THEME = -1;
-
private static final String SETUP_TRACKER_LABEL = "Setup fragment";
- private static int sTheme = DEFAULT_THEME;
-
private InputSetupRunnable mInputSetupRunnable;
private ContentFragment mContentFragment;
@@ -77,12 +74,7 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
- LayoutInflater localInflater = inflater;
- if (sTheme != -1) {
- ContextThemeWrapper themeWrapper = new ContextThemeWrapper(getActivity(), sTheme);
- localInflater = inflater.cloneInContext(themeWrapper);
- }
- View view = super.onCreateView(localInflater, container, savedInstanceState);
+ View view = super.onCreateView(inflater, container, savedInstanceState);
TvApplication.getSingletons(getActivity()).getTracker().sendScreenView(SETUP_TRACKER_LABEL);
return view;
}
@@ -97,10 +89,10 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
@Override
protected SetupGuidedStepFragment onCreateContentFragment() {
mContentFragment = new ContentFragment();
+ mContentFragment.setParentFragment(this);
Bundle arguments = new Bundle();
arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true);
mContentFragment.setArguments(arguments);
- mContentFragment.setParentFragment(this);
return mContentFragment;
}
@@ -110,13 +102,6 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
}
/**
- * Sets the custom theme dynamically.
- */
- public static void setTheme(int theme) {
- sTheme = theme;
- }
-
- /**
* Call this method to run customized input setup.
*
* @param runnable runnable to be called when the input setup is necessary.
@@ -173,6 +158,11 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
handleInputChanged();
}
+ @Override
+ public void onInputUpdated(String inputId) {
+ handleInputChanged();
+ }
+
private void handleInputChanged() {
// The actions created while enter transition is running will not be included in the
// fragment transition.
@@ -395,11 +385,6 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
updateActions();
}
- @Override
- public int onProvideTheme() {
- return sTheme == DEFAULT_THEME ? super.onProvideTheme() : sTheme;
- }
-
void executePendingAction() {
switch (mPendingAction) {
case PENDING_ACTION_INPUT_CHANGED:
diff --git a/src/com/android/tv/onboarding/WelcomeFragment.java b/src/com/android/tv/onboarding/WelcomeFragment.java
index ed85df68..00f7fe8d 100644
--- a/src/com/android/tv/onboarding/WelcomeFragment.java
+++ b/src/com/android/tv/onboarding/WelcomeFragment.java
@@ -20,8 +20,11 @@ import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
+import android.app.Activity;
+import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
+import android.support.v17.leanback.app.OnboardingFragment;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@@ -31,7 +34,6 @@ import android.widget.ImageView;
import com.android.tv.R;
import com.android.tv.common.ui.setup.SetupActionHelper;
import com.android.tv.common.ui.setup.animation.SetupAnimationHelper;
-import com.android.tv.common.ui.setup.leanback.OnboardingFragment;
import java.util.ArrayList;
import java.util.List;
@@ -580,7 +582,6 @@ public class WelcomeFragment extends OnboardingFragment {
private ImageView mArrowView;
private Animator mAnimator;
- private boolean mNeedToEndAnimator;
public WelcomeFragment() {
setExitTransition(new SetupAnimationHelper.TransitionBuilder()
@@ -589,16 +590,63 @@ public class WelcomeFragment extends OnboardingFragment {
.build());
}
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ initialize();
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ initialize();
+ }
+
+ private void initialize() {
+ if (mPageTitles == null) {
+ mPageTitles = getResources().getStringArray(R.array.welcome_page_titles);
+ mPageDescriptions = getResources().getStringArray(R.array.welcome_page_descriptions);
+ }
+ }
+
@Nullable
@Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- mPageTitles = getResources().getStringArray(R.array.welcome_page_titles);
- mPageDescriptions = getResources().getStringArray(R.array.welcome_page_descriptions);
- return super.onCreateView(inflater, container, savedInstanceState);
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+ setLogoResourceId(R.drawable.splash_logo);
+ if (savedInstanceState != null) {
+ switch (getCurrentPageIndex()) {
+ case 0:
+ mTvContentView.setImageResource(
+ TV_FRAMES_1_START[TV_FRAMES_1_START.length - 1]);
+ break;
+ case 1:
+ mTvContentView.setImageResource(
+ TV_FRAMES_2_START[TV_FRAMES_2_START.length - 1]);
+ break;
+ case 2:
+ mTvContentView.setImageResource(
+ TV_FRAMES_3_ORANGE_START[TV_FRAMES_3_ORANGE_START.length - 1]);
+ mArrowView.setImageResource(TV_FRAMES_3_BLUE_ARROW[0]);
+ break;
+ case 3:
+ default:
+ mTvContentView.setImageResource(
+ TV_FRAMES_4_START[TV_FRAMES_4_START.length - 1]);
+ break;
+ }
+ }
+ return view;
+ }
+
+ @Override
+ public int onProvideTheme() {
+ return R.style.Theme_Leanback_Onboarding;
}
@Override
- protected void onStartEnterAnimation() {
+ protected Animator onCreateEnterAnimation() {
List<Animator> animators = new ArrayList<>();
// Cloud 1
View view = getActivity().findViewById(R.id.cloud1);
@@ -640,9 +688,7 @@ public class WelcomeFragment extends OnboardingFragment {
animators.add(animator);
AnimatorSet set = new AnimatorSet();
set.playTogether(animators);
- mAnimator = set;
- mAnimator.start();
- mNeedToEndAnimator = true;
+ return set;
}
@Nullable
@@ -683,23 +729,14 @@ public class WelcomeFragment extends OnboardingFragment {
}
@Override
- protected int getLogoResourceId() {
- return R.drawable.splash_logo;
- }
-
- @Override
protected void onFinishFragment() {
SetupActionHelper.onActionClick(WelcomeFragment.this, ACTION_CATEGORY, ACTION_NEXT);
}
@Override
- protected void onStartPageChangeAnimation(int previousPage) {
+ protected void onPageChanged(int newPage, int previousPage) {
if (mAnimator != null) {
- if (mNeedToEndAnimator) {
- mAnimator.end();
- } else {
- mAnimator.cancel();
- }
+ mAnimator.cancel();
}
mArrowView.setVisibility(View.GONE);
// TV screen hiding animator.
@@ -710,7 +747,7 @@ public class WelcomeFragment extends OnboardingFragment {
// TV screen showing animator.
AnimatorSet animatorSet = new AnimatorSet();
int firstFrame;
- switch (getCurrentPageIndex()) {
+ switch (newPage) {
case 0:
animatorSet.playSequentially(hideAnimator,
SetupAnimationHelper.createFrameAnimator(mTvContentView,
@@ -762,6 +799,5 @@ public class WelcomeFragment extends OnboardingFragment {
});
mAnimator = SetupAnimationHelper.applyAnimationTimeScale(animatorSet);
mAnimator.start();
- mNeedToEndAnimator = false;
}
}
diff --git a/src/com/android/tv/parental/ContentRatingSystem.java b/src/com/android/tv/parental/ContentRatingSystem.java
index 6c00ee11..6b5d6635 100644
--- a/src/com/android/tv/parental/ContentRatingSystem.java
+++ b/src/com/android/tv/parental/ContentRatingSystem.java
@@ -490,6 +490,19 @@ public class ContentRatingSystem {
mRatingOrder = ratingOrder;
}
+ /**
+ * Returns index of the rating in this order.
+ * Returns -1 if this order doesn't contain the rating.
+ */
+ public int getRatingIndex(Rating rating) {
+ for (int i = 0; i < mRatingOrder.size(); i++) {
+ if (mRatingOrder.get(i).getName().equals(rating.getName())) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
public static class Builder {
private final List<String> mRatingNames = new ArrayList<>();
diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java
index 3cd6186c..da88f70d 100644
--- a/src/com/android/tv/receiver/BootCompletedReceiver.java
+++ b/src/com/android/tv/receiver/BootCompletedReceiver.java
@@ -21,6 +21,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.support.v4.os.BuildCompat;
import android.util.Log;
import com.android.tv.Features;
@@ -72,8 +73,7 @@ public class BootCompletedReceiver extends BroadcastReceiver {
}
}
- // DVR
- if (CommonFeatures.DVR.isEnabled(context)) {
+ if (CommonFeatures.DVR.isEnabled(context) && BuildCompat.isAtLeastN()) {
DvrRecordingService.startService(context);
}
}
diff --git a/src/com/android/tv/receiver/GlobalKeyReceiver.java b/src/com/android/tv/receiver/GlobalKeyReceiver.java
index bd81cee3..2e19c089 100644
--- a/src/com/android/tv/receiver/GlobalKeyReceiver.java
+++ b/src/com/android/tv/receiver/GlobalKeyReceiver.java
@@ -19,6 +19,7 @@ package com.android.tv.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.media.tv.TvContract;
import android.util.Log;
import android.view.KeyEvent;
@@ -39,10 +40,22 @@ public class GlobalKeyReceiver extends BroadcastReceiver {
if (DEBUG) Log.d(TAG, "onReceive: " + event);
int keyCode = event.getKeyCode();
int action = event.getAction();
- if (keyCode == KeyEvent.KEYCODE_TV && action == KeyEvent.ACTION_UP) {
- ((TvApplication) context.getApplicationContext()).handleTvKey();
- } else if (keyCode == KeyEvent.KEYCODE_TV_INPUT && action == KeyEvent.ACTION_UP) {
- ((TvApplication) context.getApplicationContext()).handleTvInputKey();
+ if (action == KeyEvent.ACTION_UP) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_GUIDE:
+ context.startActivity(
+ new Intent(Intent.ACTION_VIEW, TvContract.Programs.CONTENT_URI));
+ break;
+ case KeyEvent.KEYCODE_TV:
+ ((TvApplication) context.getApplicationContext()).handleTvKey();
+ break;
+ case KeyEvent.KEYCODE_TV_INPUT:
+ ((TvApplication) context.getApplicationContext()).handleTvInputKey();
+ break;
+ default:
+ // Do nothing
+ break;
+ }
}
}
}
diff --git a/src/com/android/tv/receiver/PackageIntentsReceiver.java b/src/com/android/tv/receiver/PackageIntentsReceiver.java
index 67f0529f..4c850402 100644
--- a/src/com/android/tv/receiver/PackageIntentsReceiver.java
+++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java
@@ -17,59 +17,18 @@
package com.android.tv.receiver;
import android.content.BroadcastReceiver;
-import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.PackageManager;
-import com.android.tv.TvActivity;
import com.android.tv.TvApplication;
-import com.android.usbtuner.setup.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 {
- private PackageManager mPackageManager;
- private ComponentName mTvActivityComponentName;
- private ComponentName mUsbTunerComponentName;
-
- private void init(Context context) {
- mPackageManager = context.getPackageManager();
- mTvActivityComponentName = new ComponentName(context, TvActivity.class);
- mUsbTunerComponentName = new ComponentName(context, UsbTunerTvInputService.class);
- }
@Override
public void onReceive(Context context, Intent intent) {
- if (mPackageManager == null) {
- init(context);
- }
((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);
- }
- }
-
- /**
- * Launches the setup activity of USB tuner TV input service.
- *
- * @param context {@link Context} instance
- */
- 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/NotificationService.java b/src/com/android/tv/recommendation/NotificationService.java
index c6a0c3f6..0095482d 100644
--- a/src/com/android/tv/recommendation/NotificationService.java
+++ b/src/com/android/tv/recommendation/NotificationService.java
@@ -28,6 +28,7 @@ import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.media.tv.TvInputInfo;
+import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
@@ -51,6 +52,7 @@ import com.android.tv.data.Program;
import com.android.tv.util.BitmapUtils;
import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
import com.android.tv.util.ImageLoader;
+import com.android.tv.util.PermissionUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -127,6 +129,12 @@ public class NotificationService extends Service implements Recommender.Listener
public void onCreate() {
if (DEBUG) Log.d(TAG, "onCreate");
super.onCreate();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
+ && !PermissionUtils.hasAccessAllEpg(this)) {
+ Log.w(TAG, "Live TV requires the system permission on this platform.");
+ stopSelf();
+ return;
+ }
mCurrentNotificationCount = 0;
mNotificationChannels = new long[NOTIFICATION_COUNT];
diff --git a/src/com/android/tv/recommendation/RecommendationDataManager.java b/src/com/android/tv/recommendation/RecommendationDataManager.java
index 66dd9fe4..a7d4c46d 100644
--- a/src/com/android/tv/recommendation/RecommendationDataManager.java
+++ b/src/com/android/tv/recommendation/RecommendationDataManager.java
@@ -16,7 +16,6 @@
package com.android.tv.recommendation;
-import android.content.ContentUris;
import android.content.Context;
import android.content.UriMatcher;
import android.database.ContentObserver;
@@ -35,8 +34,10 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
+import com.android.tv.TvApplication;
import com.android.tv.common.WeakHandler;
import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.Program;
import com.android.tv.data.WatchedHistoryManager;
import com.android.tv.util.PermissionUtils;
@@ -51,8 +52,6 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class RecommendationDataManager implements WatchedHistoryManager.Listener {
- private static final String TAG = "RecommendationDataManager";
-
private static final UriMatcher sUriMatcher;
private static final int MATCH_CHANNEL = 1;
private static final int MATCH_CHANNEL_ID = 2;
@@ -66,19 +65,15 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
private static final int MSG_START = 1000;
private static final int MSG_STOP = 1001;
- private static final int MSG_UPDATE_CHANNEL = 1002;
- private static final int MSG_UPDATE_CHANNELS = 1003;
- private static final int MSG_UPDATE_WATCH_HISTORY = 1004;
- private static final int MSG_NOTIFY_CHANNEL_RECORD_MAP_LOADED = 1005;
- private static final int MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED = 1006;
+ private static final int MSG_UPDATE_CHANNELS = 1002;
+ private static final int MSG_UPDATE_WATCH_HISTORY = 1003;
+ private static final int MSG_NOTIFY_CHANNEL_RECORD_MAP_LOADED = 1004;
+ private static final int MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED = 1005;
private static final int MSG_FIRST = MSG_START;
private static final int MSG_LAST = MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED;
- private static final int INVALID_INDEX = -1;
-
private static RecommendationDataManager sManager;
- private final static Object sListenerLock = new Object();
private final ContentObserver mContentObserver;
private final Map<Long, ChannelRecord> mChannelRecordMap = new ConcurrentHashMap<>();
private final Map<Long, ChannelRecord> mAvailableChannelRecordMap = new ConcurrentHashMap<>();
@@ -98,10 +93,33 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
private final HandlerThread mHandlerThread;
private final Handler mHandler;
+ private final Handler mMainHandler;
@Nullable
private WatchedHistoryManager mWatchedHistoryManager;
+ private final ChannelDataManager mChannelDataManager;
+ private final ChannelDataManager.Listener mChannelDataListener =
+ new ChannelDataManager.Listener() {
+ @Override
+ @MainThread
+ public void onLoadFinished() {
+ updateChannelData();
+ }
- private final List<ListenerRecord> mListeners = new ArrayList<>();
+ @Override
+ @MainThread
+ public void onChannelListUpdated() {
+ updateChannelData();
+ }
+
+ @Override
+ @MainThread
+ public void onChannelBrowsableChanged() {
+ updateChannelData();
+ }
+ };
+
+ // For thread safety, this variable is handled only on main thread.
+ private final List<Listener> mListeners = new ArrayList<>();
/**
* Gets instance of RecommendationDataManager, and adds a {@link Listener}.
@@ -112,25 +130,11 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
public synchronized static RecommendationDataManager acquireManager(
Context context, @NonNull Listener listener) {
if (sManager == null) {
- sManager = new RecommendationDataManager(context);
+ sManager = new RecommendationDataManager(context, listener);
}
- sManager.addListener(listener);
- sManager.start();
return sManager;
}
- /**
- * Removes the {@link Listener}, and releases RecommendationDataManager
- * if there are no listeners remained.
- */
- public void release(@NonNull Listener listener) {
- removeListener(listener);
- synchronized (sListenerLock) {
- if (mListeners.size() == 0) {
- stop();
- }
- }
- }
private final TvInputCallback mInternalCallback =
new TvInputCallback() {
@Override
@@ -187,12 +191,37 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
public void onInputUpdated(String inputId) { }
};
- private RecommendationDataManager(Context context) {
+ private RecommendationDataManager(Context context, final Listener listener) {
mContext = context.getApplicationContext();
mHandlerThread = new HandlerThread("RecommendationDataManager");
mHandlerThread.start();
mHandler = new RecommendationHandler(mHandlerThread.getLooper(), this);
+ mMainHandler = new RecommendationMainHandler(Looper.getMainLooper(), this);
mContentObserver = new RecommendationContentObserver(mHandler);
+ mChannelDataManager = TvApplication.getSingletons(mContext).getChannelDataManager();
+ runOnMainThread(new Runnable() {
+ @Override
+ public void run() {
+ addListener(listener);
+ start();
+ }
+ });
+ }
+
+ /**
+ * Removes the {@link Listener}, and releases RecommendationDataManager
+ * if there are no listeners remained.
+ */
+ public void release(@NonNull final Listener listener) {
+ runOnMainThread(new Runnable() {
+ @Override
+ public void run() {
+ removeListener(listener);
+ if (mListeners.size() == 0) {
+ stop();
+ }
+ }
+ });
}
/**
@@ -216,54 +245,48 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
return Collections.unmodifiableCollection(mAvailableChannelRecordMap.values());
}
+ @MainThread
private void start() {
mHandler.sendEmptyMessage(MSG_START);
+ mChannelDataManager.addListener(mChannelDataListener);
+ if (mChannelDataManager.isDbLoadFinished()) {
+ updateChannelData();
+ }
}
+ @MainThread
private void stop() {
for (int what = MSG_FIRST; what <= MSG_LAST; ++what) {
mHandler.removeMessages(what);
}
+ mChannelDataManager.removeListener(mChannelDataListener);
mHandler.sendEmptyMessage(MSG_STOP);
mHandlerThread.quitSafely();
+ mMainHandler.removeCallbacksAndMessages(null);
sManager = null;
}
- private int getListenerIndexLocked(Listener listener) {
- for (int i = 0; i < mListeners.size(); ++i) {
- if (mListeners.get(i).mListener == listener) {
- return i;
- }
- }
- return INVALID_INDEX;
+ @MainThread
+ private void updateChannelData() {
+ mHandler.removeMessages(MSG_UPDATE_CHANNELS);
+ mHandler.obtainMessage(MSG_UPDATE_CHANNELS, mChannelDataManager.getBrowsableChannelList())
+ .sendToTarget();
}
+ @MainThread
private void addListener(Listener listener) {
- synchronized (sListenerLock) {
- if (getListenerIndexLocked(listener) == INVALID_INDEX) {
- mListeners.add((new ListenerRecord(listener)));
- }
- }
+ mListeners.add(listener);
}
+ @MainThread
private void removeListener(Listener listener) {
- synchronized (sListenerLock) {
- int idx = getListenerIndexLocked(listener);
- if (idx != INVALID_INDEX) {
- ListenerRecord record = mListeners.remove(idx);
- record.mListener = null;
- }
- }
+ mListeners.remove(listener);
}
private void onStart() {
if (!mStarted) {
mStarted = true;
mCancelLoadTask = false;
- mContext.getContentResolver().registerContentObserver(
- TvContract.Channels.CONTENT_URI, true, mContentObserver);
- mHandler.obtainMessage(MSG_UPDATE_CHANNELS, TvContract.Channels.CONTENT_URI)
- .sendToTarget();
if (!PermissionUtils.hasAccessWatchedHistory(mContext)) {
mWatchedHistoryManager = new WatchedHistoryManager(mContext);
mWatchedHistoryManager.setListener(this);
@@ -297,42 +320,7 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
}
@WorkerThread
- private void onUpdateChannel(Uri uri) {
- Channel channel = null;
- try (Cursor cursor = mContext.getContentResolver().query(uri, Channel.PROJECTION,
- null, null, null)) {
- if (cursor != null && cursor.moveToFirst()) {
- channel = Channel.fromCursor(cursor);
- }
- }
- boolean isChannelRecordMapChanged = false;
- if (channel == null) {
- long channelId = ContentUris.parseId(uri);
- mChannelRecordMap.remove(channelId);
- isChannelRecordMapChanged = mAvailableChannelRecordMap.remove(channelId) != null;
- } else if (updateChannelRecordMapFromChannel(channel)) {
- isChannelRecordMapChanged = true;
- }
- if (isChannelRecordMapChanged && mChannelRecordMapLoaded
- && !mHandler.hasMessages(MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED)) {
- mHandler.sendEmptyMessage(MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED);
- }
- }
-
- @WorkerThread
- private void onUpdateChannels(Uri uri) {
- List<Channel> channels = new ArrayList<>();
- try (Cursor cursor = mContext.getContentResolver().query(uri, Channel.PROJECTION,
- null, null, null)) {
- if (cursor != null) {
- while (cursor.moveToNext()) {
- if (mCancelLoadTask) {
- return;
- }
- channels.add(Channel.fromCursor(cursor));
- }
- }
- }
+ private void onUpdateChannels(List<Channel> channels) {
boolean isChannelRecordMapChanged = false;
Set<Long> removedChannelIdSet = new HashSet<>(mChannelRecordMap.keySet());
// Builds removedChannelIdSet.
@@ -374,11 +362,14 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
final ChannelRecord channelRecord =
updateChannelRecordFromWatchedProgram(watchedProgram);
if (mChannelRecordMapLoaded && channelRecord != null) {
- synchronized (sListenerLock) {
- for (ListenerRecord l : mListeners) {
- l.postNewWatchLog(channelRecord);
+ runOnMainThread(new Runnable() {
+ @Override
+ public void run() {
+ for (Listener l : mListeners) {
+ l.onNewWatchLog(channelRecord);
+ }
}
- }
+ });
}
}
if (!mChannelRecordMapLoaded) {
@@ -410,14 +401,17 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
@Override
public void onNewRecordAdded(WatchedHistoryManager.WatchedRecord watchedRecord) {
- ChannelRecord channelRecord = updateChannelRecordFromWatchedProgram(
+ final ChannelRecord channelRecord = updateChannelRecordFromWatchedProgram(
convertFromWatchedHistoryManagerRecords(watchedRecord));
if (mChannelRecordMapLoaded && channelRecord != null) {
- synchronized (sListenerLock) {
- for (ListenerRecord l : mListeners) {
- l.postNewWatchLog(channelRecord);
+ runOnMainThread(new Runnable() {
+ @Override
+ public void run() {
+ for (Listener l : mListeners) {
+ l.onNewWatchLog(channelRecord);
+ }
}
- }
+ });
}
}
@@ -452,19 +446,25 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
private void onNotifyChannelRecordMapLoaded() {
mChannelRecordMapLoaded = true;
- synchronized (sListenerLock) {
- for (ListenerRecord l : mListeners) {
- l.postChannelRecordLoaded();
+ runOnMainThread(new Runnable() {
+ @Override
+ public void run() {
+ for (Listener l : mListeners) {
+ l.onChannelRecordLoaded();
+ }
}
- }
+ });
}
private void onNotifyChannelRecordMapChanged() {
- synchronized (sListenerLock) {
- for (ListenerRecord l : mListeners) {
- l.postChannelRecordChanged();
+ runOnMainThread(new Runnable() {
+ @Override
+ public void run() {
+ for (Listener l : mListeners) {
+ l.onChannelRecordChanged();
+ }
}
- }
+ });
}
/**
@@ -511,15 +511,6 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
@Override
public void onChange(final boolean selfChange, final Uri uri) {
switch (sUriMatcher.match(uri)) {
- case MATCH_CHANNEL:
- if (!mHandler.hasMessages(MSG_UPDATE_CHANNELS, TvContract.Channels.CONTENT_URI)) {
- mHandler.obtainMessage(MSG_UPDATE_CHANNELS, TvContract.Channels.CONTENT_URI)
- .sendToTarget();
- }
- break;
- case MATCH_CHANNEL_ID:
- mHandler.obtainMessage(MSG_UPDATE_CHANNEL, uri).sendToTarget();
- break;
case MATCH_WATCHED_PROGRAM_ID:
if (!mHandler.hasMessages(MSG_UPDATE_WATCH_HISTORY,
TvContract.WatchedPrograms.CONTENT_URI)) {
@@ -530,6 +521,14 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
}
}
+ private void runOnMainThread(Runnable r) {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ r.run();
+ } else {
+ mMainHandler.post(r);
+ }
+ }
+
/**
* A listener interface to receive notification about the recommendation data.
*
@@ -561,55 +560,6 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
void onChannelRecordChanged();
}
- private static class ListenerRecord {
- private Listener mListener;
- private final Handler mHandler;
-
- public ListenerRecord(Listener listener) {
- mHandler = new Handler();
- mListener = listener;
- }
-
- public void postChannelRecordLoaded() {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- synchronized (sListenerLock) {
- if (mListener != null) {
- mListener.onChannelRecordLoaded();
- }
- }
- }
- });
- }
-
- public void postNewWatchLog(final ChannelRecord channelRecord) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- synchronized (sListenerLock) {
- if (mListener != null) {
- mListener.onNewWatchLog(channelRecord);
- }
- }
- }
- });
- }
-
- public void postChannelRecordChanged() {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- synchronized (sListenerLock) {
- if (mListener != null) {
- mListener.onChannelRecordChanged();
- }
- }
- }
- });
- }
- }
-
private static class RecommendationHandler extends WeakHandler<RecommendationDataManager> {
public RecommendationHandler(@NonNull Looper looper, RecommendationDataManager ref) {
super(looper, ref);
@@ -626,14 +576,9 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
dataManager.onStop();
}
break;
- case MSG_UPDATE_CHANNEL:
- if (dataManager.mStarted) {
- dataManager.onUpdateChannel((Uri) msg.obj);
- }
- break;
case MSG_UPDATE_CHANNELS:
if (dataManager.mStarted) {
- dataManager.onUpdateChannels((Uri) msg.obj);
+ dataManager.onUpdateChannels((List<Channel>) msg.obj);
}
break;
case MSG_UPDATE_WATCH_HISTORY:
@@ -654,4 +599,13 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
}
}
}
+
+ private static class RecommendationMainHandler extends WeakHandler<RecommendationDataManager> {
+ public RecommendationMainHandler(@NonNull Looper looper, RecommendationDataManager ref) {
+ super(looper, ref);
+ }
+
+ @Override
+ protected void handleMessage(Message msg, @NonNull RecommendationDataManager referent) { }
+ }
}
diff --git a/src/com/android/tv/recommendation/Recommender.java b/src/com/android/tv/recommendation/Recommender.java
index 0561449e..82c2893d 100644
--- a/src/com/android/tv/recommendation/Recommender.java
+++ b/src/com/android/tv/recommendation/Recommender.java
@@ -145,7 +145,7 @@ public class Recommender implements RecommendationDataManager.Listener {
mChannelSortKey.put(records.get(i).first.getId(), String.format(sortKeyFormat, i));
results.add(records.get(i).first);
}
- return Collections.unmodifiableList(results);
+ return results;
}
/**
diff --git a/src/com/android/tv/recommendation/RoutineWatchEvaluator.java b/src/com/android/tv/recommendation/RoutineWatchEvaluator.java
index 694da6bf..5ff7cae9 100644
--- a/src/com/android/tv/recommendation/RoutineWatchEvaluator.java
+++ b/src/com/android/tv/recommendation/RoutineWatchEvaluator.java
@@ -16,7 +16,9 @@
package com.android.tv.recommendation;
+import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
import com.android.tv.data.Program;
@@ -36,7 +38,6 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator {
private static final double TIME_MATCH_WEIGHT = 1 - TITLE_MATCH_WEIGHT;
private static final long DIFF_MS_TOLERANCE_FOR_OLD_PROGRAM = TimeUnit.DAYS.toMillis(14);
private static final long MAX_DIFF_MS_FOR_OLD_PROGRAM = TimeUnit.DAYS.toMillis(56);
- private static final String REGULAR_EXPRESSION_FOR_WHITE_SPACES = "\\s+";
@Override
public double evaluateChannel(long channelId) {
@@ -91,8 +92,8 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator {
return maxScore;
}
- private double calculateRoutineWatchScore(
- Program currentProgram, Program watchedProgram, long watchedDurationMs) {
+ private static double calculateRoutineWatchScore(Program currentProgram, Program watchedProgram,
+ long watchedDurationMs) {
double timeMatchScore = calculateTimeMatchScore(currentProgram, watchedProgram);
double titleMatchScore = calculateTitleMatchScore(
currentProgram.getTitle(), watchedProgram.getTitle());
@@ -107,10 +108,16 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator {
* watchDurationScore * multiplierForOldProgram;
}
- private double calculateTitleMatchScore(String title1, String title2) {
+ @VisibleForTesting
+ static double calculateTitleMatchScore(@Nullable String title1, @Nullable String title2) {
+ if (TextUtils.isEmpty(title1) || TextUtils.isEmpty(title2)) {
+ return 0;
+ }
List<String> wordList1 = splitTextToWords(title1);
List<String> wordList2 = splitTextToWords(title2);
-
+ if (wordList1.isEmpty() || wordList2.isEmpty()) {
+ return 0;
+ }
int maxMatchedWordSeqLen = calculateMaximumMatchedWordSequenceLength(
wordList1, wordList2);
@@ -121,8 +128,8 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator {
}
@VisibleForTesting
- int calculateMaximumMatchedWordSequenceLength(
- List<String> toSearchWords, List<String> toMatchWords) {
+ static int calculateMaximumMatchedWordSequenceLength(List<String> toSearchWords,
+ List<String> toMatchWords) {
int[] matchedWordSeqLen = new int[toMatchWords.size()];
int maxMatchedWordSeqLen = 0;
for (String word : toSearchWords) {
@@ -142,7 +149,7 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator {
return maxMatchedWordSeqLen;
}
- private double calculateTimeMatchScore(Program p1, Program p2) {
+ private static double calculateTimeMatchScore(Program p1, Program p2) {
ProgramTime t1 = ProgramTime.createFromProgram(p1);
ProgramTime t2 = ProgramTime.createFromProgram(p2);
@@ -155,7 +162,7 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator {
}
@VisibleForTesting
- double calculateOverlappedIntervalScore(ProgramTime t1, ProgramTime t2) {
+ static double calculateOverlappedIntervalScore(ProgramTime t1, ProgramTime t2) {
if (t1.dayChanged && !t2.dayChanged) {
// Swap two values.
return calculateOverlappedIntervalScore(t2, t1);
@@ -181,7 +188,7 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator {
return score;
}
- private double calculateWatchDurationScore(Program program, long durationMs) {
+ private static double calculateWatchDurationScore(Program program, long durationMs) {
return (double) durationMs
/ (program.getEndTimeUtcMillis() - program.getStartTimeUtcMillis());
}
diff --git a/src/com/android/tv/ui/AppLayerTvView.java b/src/com/android/tv/ui/AppLayerTvView.java
index befa004c..c7b94a15 100644
--- a/src/com/android/tv/ui/AppLayerTvView.java
+++ b/src/com/android/tv/ui/AppLayerTvView.java
@@ -16,9 +16,8 @@
package com.android.tv.ui;
-import com.android.tv.common.recording.PlaybackTvView;
-
import android.content.Context;
+import android.media.tv.TvView;
import android.util.AttributeSet;
/**
@@ -30,7 +29,7 @@ import android.util.AttributeSet;
* TODO: remove this class once the TvView.setMain() is revisited.
* </p>
*/
-public class AppLayerTvView extends PlaybackTvView {
+public class AppLayerTvView extends TvView {
public AppLayerTvView(Context context) {
super(context);
}
diff --git a/src/com/android/tv/ui/BlockScreenView.java b/src/com/android/tv/ui/BlockScreenView.java
new file mode 100644
index 00000000..52b9389d
--- /dev/null
+++ b/src/com/android/tv/ui/BlockScreenView.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.ui;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.tv.R;
+import com.android.tv.ui.TunableTvView.BlockScreenType;
+
+public class BlockScreenView extends LinearLayout {
+ private View mContainerView;
+ private View mImageContainer;
+ private ImageView mNormalImageView;
+ private ImageView mShrunkenImageView;
+ private View mSpace;
+ private TextView mTextView;
+
+ private final int mSpacingNormal;
+ private final int mSpacingShrunken;
+
+ // Animators used for fade in/out of block screen icon.
+ private Animator mFadeIn;
+ private Animator mFadeOut;
+
+ public BlockScreenView(Context context) {
+ this(context, null, 0);
+ }
+
+ public BlockScreenView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public BlockScreenView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mSpacingNormal = getResources().getDimensionPixelOffset(
+ R.dimen.tvview_block_vertical_spacing);
+ mSpacingShrunken = getResources().getDimensionPixelOffset(
+ R.dimen.shrunken_tvview_block_vertical_spacing);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mContainerView = findViewById(R.id.block_screen_container);
+ mImageContainer = findViewById(R.id.image_container);
+ mNormalImageView = (ImageView) findViewById(R.id.block_screen_icon);
+ mShrunkenImageView = (ImageView) findViewById(R.id.block_screen_shrunken_icon);
+ mSpace = findViewById(R.id.space);
+ mTextView = (TextView) findViewById(R.id.block_screen_text);
+ mFadeIn = AnimatorInflater.loadAnimator(getContext(),
+ R.animator.tvview_block_screen_fade_in);
+ mFadeIn.setTarget(mContainerView);
+ mFadeOut = AnimatorInflater.loadAnimator(getContext(),
+ R.animator.tvview_block_screen_fade_out);
+ mFadeOut.setTarget(mContainerView);
+ mFadeOut.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mContainerView.setVisibility(GONE);
+ mContainerView.setAlpha(1f);
+ }
+ });
+ }
+
+ /**
+ * Sets the normal image.
+ */
+ public void setImage(int resId) {
+ mNormalImageView.setImageResource(resId);
+ updateSpaceVisibility();
+ }
+
+ /**
+ * Sets the scale type of the normal image.
+ */
+ public void setScaleType(ScaleType scaleType) {
+ mNormalImageView.setScaleType(scaleType);
+ updateSpaceVisibility();
+ }
+
+ /**
+ * Sets the shrunken image.
+ */
+ public void setShrunkenImage(int resId) {
+ mShrunkenImageView.setImageResource(resId);
+ updateSpaceVisibility();
+ }
+
+ /**
+ * Show or hide the image of this view.
+ */
+ public void setImageVisibility(boolean visible) {
+ mImageContainer.setVisibility(visible ? VISIBLE : GONE);
+ updateSpaceVisibility();
+ }
+
+ /**
+ * Sets the text message.
+ */
+ public void setText(int resId) {
+ mTextView.setText(resId);
+ updateSpaceVisibility();
+ }
+
+ /**
+ * Sets the text message.
+ */
+ public void setText(String text) {
+ mTextView.setText(text);
+ updateSpaceVisibility();
+ }
+
+ private void updateSpaceVisibility() {
+ if (isImageViewVisible() && isTextViewVisible(mTextView)) {
+ mSpace.setVisibility(VISIBLE);
+ } else {
+ mSpace.setVisibility(GONE);
+ }
+ }
+
+ private boolean isImageViewVisible() {
+ return mImageContainer.getVisibility() == VISIBLE
+ && (isImageViewVisible(mNormalImageView) || isImageViewVisible(mShrunkenImageView));
+ }
+
+ private static boolean isImageViewVisible(ImageView imageView) {
+ return imageView.getVisibility() != GONE && imageView.getDrawable() != null;
+ }
+
+ private static boolean isTextViewVisible(TextView textView) {
+ return textView.getVisibility() != GONE && !TextUtils.isEmpty(textView.getText());
+ }
+
+ /**
+ * Changes the spacing between the image view and the text view according to the
+ * {@code blockScreenType}.
+ */
+ public void setSpacing(@BlockScreenType int blockScreenType) {
+ mSpace.getLayoutParams().height =
+ blockScreenType == TunableTvView.BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW
+ ? mSpacingShrunken : mSpacingNormal;
+ requestLayout();
+ }
+
+ /**
+ * Changes the view layout according to the {@code blockScreenType}.
+ */
+ public void onBlockStatusChanged(@BlockScreenType int blockScreenType, boolean withAnimation) {
+ if (!withAnimation) {
+ switch (blockScreenType) {
+ case TunableTvView.BLOCK_SCREEN_TYPE_NO_UI:
+ mContainerView.setVisibility(GONE);
+ break;
+ case TunableTvView.BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
+ mNormalImageView.setVisibility(GONE);
+ mShrunkenImageView.setVisibility(VISIBLE);
+ mContainerView.setVisibility(VISIBLE);
+ break;
+ case TunableTvView.BLOCK_SCREEN_TYPE_NORMAL:
+ mNormalImageView.setVisibility(VISIBLE);
+ mShrunkenImageView.setVisibility(GONE);
+ mContainerView.setVisibility(VISIBLE);
+ break;
+ }
+ } else {
+ switch (blockScreenType) {
+ case TunableTvView.BLOCK_SCREEN_TYPE_NO_UI:
+ if (mContainerView.getVisibility() == VISIBLE) {
+ mFadeOut.start();
+ }
+ break;
+ case TunableTvView.BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
+ mNormalImageView.setVisibility(GONE);
+ mShrunkenImageView.setVisibility(VISIBLE);
+ mContainerView.setVisibility(VISIBLE);
+ if (mContainerView.getVisibility() == GONE) {
+ mFadeIn.start();
+ }
+ break;
+ case TunableTvView.BLOCK_SCREEN_TYPE_NORMAL:
+ mNormalImageView.setVisibility(VISIBLE);
+ mShrunkenImageView.setVisibility(GONE);
+ mContainerView.setVisibility(VISIBLE);
+ if (mContainerView.getVisibility() == GONE) {
+ mFadeIn.start();
+ }
+ break;
+ }
+ }
+ updateSpaceVisibility();
+ }
+
+ /**
+ * Scales the contents view by the given {@code scale}.
+ */
+ public void scaleContainerView(float scale) {
+ mContainerView.setScaleX(scale);
+ mContainerView.setScaleY(scale);
+ }
+
+ public void addFadeOutAnimationListener(AnimatorListener listener) {
+ mFadeOut.addListener(listener);
+ }
+
+ /**
+ * Ends the currently running animations.
+ */
+ public void endAnimations() {
+ if (mFadeIn != null && mFadeIn.isRunning()) {
+ mFadeIn.end();
+ }
+ if (mFadeOut != null && mFadeOut.isRunning()) {
+ mFadeOut.end();
+ }
+ }
+}
diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java
index 17ac8f3b..a36ba83c 100644
--- a/src/com/android/tv/ui/ChannelBannerView.java
+++ b/src/com/android/tv/ui/ChannelBannerView.java
@@ -16,8 +16,6 @@
package com.android.tv.ui;
-import static com.android.tv.util.ImageLoader.ImageLoaderCallback;
-
import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.animation.AnimatorListenerAdapter;
@@ -35,8 +33,10 @@ import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
+import android.text.format.DateUtils;
import android.text.style.TextAppearanceSpan;
import android.util.AttributeSet;
+import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
@@ -50,11 +50,13 @@ import android.widget.TextView;
import com.android.tv.MainActivity;
import com.android.tv.R;
+import com.android.tv.common.recording.RecordedProgram;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.data.StreamInfo;
import com.android.tv.util.ImageCache;
import com.android.tv.util.ImageLoader;
+import com.android.tv.util.ImageLoader.ImageLoaderCallback;
import com.android.tv.util.ImageLoader.LoadTvInputLogoTask;
import com.android.tv.util.Utils;
@@ -66,6 +68,8 @@ import java.util.Objects;
* A view to render channel banner.
*/
public class ChannelBannerView extends FrameLayout implements TvTransitionManager.TransitionLayout {
+ private static final String TAG = "ChannelBannerView";
+ private static final boolean DEBUG = false;
/**
* Show all information at the channel banner.
@@ -111,6 +115,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
private View mAnchorView;
private Channel mCurrentChannel;
private Program mLastUpdatedProgram;
+ private RecordedProgram mLastUpdatedRecordedProgram;
private final Handler mHandler = new Handler();
private int mLockType;
@@ -235,6 +240,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
@Override
protected void onAttachedToWindow() {
+ if (DEBUG) Log.d(TAG, "onAttachedToWindow");
super.onAttachedToWindow();
getContext().getContentResolver().registerContentObserver(TvContract.Programs.CONTENT_URI,
true, mProgramUpdateObserver);
@@ -242,6 +248,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
@Override
protected void onDetachedFromWindow() {
+ if (DEBUG) Log.d(TAG, "onDetachedToWindow");
getContext().getContentResolver().unregisterContentObserver(mProgramUpdateObserver);
super.onDetachedFromWindow();
}
@@ -329,8 +336,6 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
/**
* Update channel banner view.
- * Note that this only updates the channel banner contents,
- * and use onBeforeShow() or onAfterHide() for showing/hiding.
*
* @param info A StreamInfo that includes stream information.
* If it's {@code null}, only program information will be updated.
@@ -342,19 +347,19 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
scheduleHide();
}
mCurrentChannel = channel;
- if (channel == null) {
- mLastUpdatedProgram = null;
- updateProgramInfo(null);
- return;
- }
mChannelView.setVisibility(VISIBLE);
if (info != null) {
// If the current channels between ChannelTuner and TvView are different,
// the stream information should not be seen.
- updateStreamInfo(channel.equals(info.getCurrentChannel()) ? info : null);
+ updateStreamInfo(channel != null && channel.equals(info.getCurrentChannel()) ? info
+ : null);
updateChannelInfo();
}
- updateProgramInfo(mMainActivity.getCurrentProgram());
+ if (mMainActivity.isRecordingPlayback()) {
+ updateProgramInfo(mMainActivity.getPlayingRecordedProgram());
+ } else {
+ updateProgramInfo(mMainActivity.getCurrentProgram());
+ }
}
private void updateStreamInfo(StreamInfo info) {
@@ -363,7 +368,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
updateText(mClosedCaptionTextView, info.hasClosedCaption() ? sClosedCaptionMark
: EMPTY_STRING);
updateText(mAspectRatioTextView,
- Utils.getAspectRatioString(info.getVideoWidth(), info.getVideoHeight()));
+ Utils.getAspectRatioString(info.getVideoDisplayAspectRatio()));
updateText(mResolutionTextView,
Utils.getVideoDefinitionLevelString(
mMainActivity, info.getVideoDefinitionLevel()));
@@ -380,11 +385,24 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
private void updateChannelInfo() {
// Update static information for a channel.
- String displayNumber = mCurrentChannel.getDisplayNumber();
- if (displayNumber == null) {
- displayNumber = EMPTY_STRING;
+ String displayNumber = EMPTY_STRING;
+ String displayName = EMPTY_STRING;
+ if (mCurrentChannel != null) {
+ displayNumber = mCurrentChannel.getDisplayNumber();
+ if (displayNumber == null) {
+ displayNumber = EMPTY_STRING;
+ }
+ displayName = mCurrentChannel.getDisplayName();
+ if (displayName == null) {
+ displayName = EMPTY_STRING;
+ }
}
+ if (displayNumber.isEmpty()) {
+ mChannelNumberTextView.setVisibility(GONE);
+ } else {
+ mChannelNumberTextView.setVisibility(VISIBLE);
+ }
if (displayNumber.length() <= 3) {
updateTextView(
mChannelNumberTextView,
@@ -402,14 +420,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
R.dimen.channel_banner_channel_number_small_margin_top);
}
mChannelNumberTextView.setText(displayNumber);
- String displayName = mCurrentChannel.getDisplayName();
- if (displayName == null) {
- displayName = EMPTY_STRING;
- }
mChannelNameTextView.setText(displayName);
-
TvInputInfo info = mMainActivity.getTvInputManagerHelper().getTvInputInfo(
- mCurrentChannel.getInputId());
+ getCurrentInputId());
if (info == null || !ImageLoader.loadBitmap(createTvInputLogoLoaderCallback(info, this),
new LoadTvInputLogoTask(getContext(), ImageCache.getInstance(), info))) {
mTvInputLogoImageView.setVisibility(View.GONE);
@@ -417,9 +430,24 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
}
mChannelLogoImageView.setImageBitmap(null);
mChannelLogoImageView.setVisibility(View.GONE);
- mCurrentChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO,
- mChannelLogoImageViewWidth, mChannelLogoImageViewHeight,
- createChannelLogoCallback(this, mCurrentChannel));
+ if (mCurrentChannel != null) {
+ mCurrentChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO,
+ mChannelLogoImageViewWidth, mChannelLogoImageViewHeight,
+ createChannelLogoCallback(this, mCurrentChannel));
+ }
+ }
+
+ private String getCurrentInputId() {
+ Channel channel = mMainActivity.getCurrentChannel();
+ if (channel != null) {
+ return channel.getInputId();
+ } else if (mMainActivity.isRecordingPlayback()) {
+ RecordedProgram recordedProgram = mMainActivity.getPlayingRecordedProgram();
+ if (recordedProgram != null) {
+ return recordedProgram.getInputId();
+ }
+ }
+ return null;
}
private void updateTvInputLogo(Bitmap bitmap) {
@@ -432,8 +460,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
return new ImageLoaderCallback<ChannelBannerView>(channelBannerView) {
@Override
public void onBitmapLoaded(ChannelBannerView channelBannerView, Bitmap bitmap) {
- if (bitmap != null && info.getId()
- .equals(channelBannerView.mCurrentChannel.getInputId())) {
+ if (bitmap != null && channelBannerView.mCurrentChannel != null
+ && info.getId().equals(channelBannerView.mCurrentChannel.getInputId())) {
channelBannerView.updateTvInputLogo(bitmap);
}
}
@@ -510,7 +538,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
if (mLastUpdatedProgram == null
|| !TextUtils.equals(program.getTitle(), mLastUpdatedProgram.getTitle())
|| !TextUtils.equals(program.getEpisodeDisplayTitle(getContext()),
- mLastUpdatedProgram.getEpisodeDisplayTitle(getContext()))) {
+ mLastUpdatedProgram.getEpisodeDisplayTitle(getContext()))) {
updateProgramTextView(program);
}
updateProgramTimeInfo(program);
@@ -519,7 +547,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
// cancel the animation.
boolean isProgramChanged = !Objects.equals(mLastUpdatedProgram, program);
if (mResizeAnimator != null && isProgramChanged) {
- mLastUpdatedProgram = program;
+ setLastUpdatedProgram(program);
mProgramInfoUpdatePendingByResizing = true;
mResizeAnimator.cancel();
} else if (mResizeAnimator == null) {
@@ -537,16 +565,73 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
} else {
mProgramInfoUpdatePendingByResizing = true;
}
- mLastUpdatedProgram = program;
+ setLastUpdatedProgram(program);
+ }
+
+ private void updateProgramInfo(RecordedProgram recordedProgram) {
+ if (mLockType == LOCK_CHANNEL_INFO) {
+ updateProgramInfo(sLockedChannelProgram);
+ return;
+ } else if (recordedProgram == null) {
+ updateProgramInfo(sNoProgram);
+ return;
+ }
+
+ if (mLastUpdatedRecordedProgram == null
+ || !TextUtils.equals(recordedProgram.getTitle(),
+ mLastUpdatedRecordedProgram.getTitle())
+ || !TextUtils.equals(recordedProgram.getEpisodeDisplayTitle(getContext()),
+ mLastUpdatedRecordedProgram.getEpisodeDisplayTitle(getContext()))) {
+ updateProgramTextView(recordedProgram);
+ }
+ updateProgramTimeInfo(recordedProgram);
+
+ // When the program is changed, but the previous resize animation has not ended yet,
+ // cancel the animation.
+ boolean isProgramChanged = !Objects.equals(mLastUpdatedRecordedProgram, recordedProgram);
+ if (mResizeAnimator != null && isProgramChanged) {
+ setLastUpdatedRecordedProgram(recordedProgram);
+ mProgramInfoUpdatePendingByResizing = true;
+ mResizeAnimator.cancel();
+ } else if (mResizeAnimator == null) {
+ if (mLockType != LOCK_NONE
+ || TextUtils.isEmpty(recordedProgram.getShortDescription())) {
+ mProgramDescriptionTextView.setVisibility(GONE);
+ mProgramDescriptionText = "";
+ } else {
+ mProgramDescriptionTextView.setVisibility(VISIBLE);
+ mProgramDescriptionText = recordedProgram.getShortDescription();
+ }
+ String description = mProgramDescriptionTextView.getText().toString();
+ boolean needFadeAnimation = isProgramChanged
+ || !description.equals(mProgramDescriptionText);
+ updateBannerHeight(needFadeAnimation);
+ } else {
+ mProgramInfoUpdatePendingByResizing = true;
+ }
+ setLastUpdatedRecordedProgram(recordedProgram);
}
private void updateProgramTextView(Program program) {
if (program == null) {
return;
}
+ updateProgramTextView(program == sLockedChannelProgram, program.getTitle(),
+ program.getEpisodeTitle(), program.getEpisodeDisplayTitle(getContext()));
+ }
+
+ private void updateProgramTextView(RecordedProgram recordedProgram) {
+ if (recordedProgram == null) {
+ return;
+ }
+ updateProgramTextView(false, recordedProgram.getTitle(), recordedProgram.getEpisodeTitle(),
+ recordedProgram.getEpisodeDisplayTitle(getContext()));
+ }
+ private void updateProgramTextView(boolean dimText, String title, String episodeTitle,
+ String episodeDisplayTitle) {
mProgramTextView.setVisibility(View.VISIBLE);
- if (program == sLockedChannelProgram) {
+ if (dimText) {
mProgramTextView.setTextColor(mChannelBannerDimTextColor);
} else {
mProgramTextView.setTextColor(mChannelBannerTextColor);
@@ -554,17 +639,15 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
updateTextView(mProgramTextView,
R.dimen.channel_banner_program_large_text_size,
R.dimen.channel_banner_program_large_margin_top);
- if (TextUtils.isEmpty(program.getEpisodeTitle())) {
- mProgramTextView.setText(program.getTitle());
+ if (TextUtils.isEmpty(episodeTitle)) {
+ mProgramTextView.setText(title);
} else {
- String title = program.getTitle();
- String episodeTitle = program.getEpisodeDisplayTitle(getContext());
- String fullTitle = title + " " + episodeTitle;
+ String fullTitle = title + " " + episodeDisplayTitle;
SpannableString text = new SpannableString(fullTitle);
text.setSpan(new TextAppearanceSpan(getContext(),
R.style.text_appearance_channel_banner_episode_title),
- fullTitle.length() - episodeTitle.length(), fullTitle.length(),
+ fullTitle.length() - episodeDisplayTitle.length(), fullTitle.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mProgramTextView.setText(text);
}
@@ -617,6 +700,38 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
}
}
+ private void updateProgramTimeInfo(RecordedProgram recordedProgram) {
+ long durationMs = recordedProgram.getDurationMillis();
+ if (mLockType != LOCK_CHANNEL_INFO && durationMs > 0) {
+ mProgramTimeTextView.setVisibility(View.VISIBLE);
+ mRemainingTimeView.setVisibility(View.VISIBLE);
+
+ mProgramTimeTextView.setText(DateUtils.formatElapsedTime(durationMs / 1000));
+
+ long currTimeMs = mMainActivity.getCurrentPlayingPosition();
+ if (currTimeMs <= 0) {
+ mRemainingTimeView.setProgress(0);
+ } else if (currTimeMs >= durationMs) {
+ mRemainingTimeView.setProgress(100);
+ } else {
+ mRemainingTimeView.setProgress((int) (100 * currTimeMs / durationMs));
+ }
+ } else {
+ mProgramTimeTextView.setVisibility(View.GONE);
+ mRemainingTimeView.setVisibility(View.GONE);
+ }
+ }
+
+ private void setLastUpdatedProgram(Program program) {
+ mLastUpdatedProgram = program;
+ mLastUpdatedRecordedProgram = null;
+ }
+
+ private void setLastUpdatedRecordedProgram(RecordedProgram recordedProgram) {
+ mLastUpdatedProgram = null;
+ mLastUpdatedRecordedProgram = recordedProgram;
+ }
+
private void updateBannerHeight(boolean needFadeAnimation) {
Assert.assertNull(mResizeAnimator);
// Need to measure the layout height with the new description text.
diff --git a/src/com/android/tv/ui/DialogUtils.java b/src/com/android/tv/ui/DialogUtils.java
new file mode 100644
index 00000000..acbaf8c8
--- /dev/null
+++ b/src/com/android/tv/ui/DialogUtils.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.ui;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+
+import com.android.tv.common.SoftPreconditions;
+
+public final class DialogUtils {
+
+ /**
+ * Shows a list in a Dialog.
+ *
+ * @param itemResIds String resource id for each item
+ * @param runnables Runnable for each item
+ */
+ public static void showListDialog(Context context, int[] itemResIds,
+ final Runnable[] runnables) {
+ int size = itemResIds.length;
+ SoftPreconditions.checkState(size == runnables.length);
+ DialogInterface.OnClickListener onClickListener
+ = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog, int which) {
+ Runnable runnable = runnables[which];
+ if (runnable != null) {
+ runnable.run();
+ }
+ dialog.dismiss();
+ }
+ };
+ CharSequence[] items = new CharSequence[itemResIds.length];
+ Resources res = context.getResources();
+ for (int i = 0; i < size; ++i) {
+ items[i] = res.getString(itemResIds[i]);
+ }
+ new AlertDialog.Builder(context)
+ .setItems(items, onClickListener)
+ .create()
+ .show();
+ }
+
+ private DialogUtils() { }
+}
diff --git a/src/com/android/tv/ui/KeypadChannelSwitchView.java b/src/com/android/tv/ui/KeypadChannelSwitchView.java
index cf43fc9b..abc05bad 100644
--- a/src/com/android/tv/ui/KeypadChannelSwitchView.java
+++ b/src/com/android/tv/ui/KeypadChannelSwitchView.java
@@ -41,9 +41,9 @@ 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.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelNumber;
-import com.android.tv.util.SoftPreconditions;
import java.util.ArrayList;
import java.util.List;
diff --git a/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java b/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java
index afea9ba5..63ee199d 100644
--- a/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java
+++ b/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java
@@ -35,8 +35,8 @@ public class OnRepeatedKeyInterceptListener implements VerticalGridView.OnKeyInt
private static final int[] MAX_SKIPPED_VIEW_COUNT = { 1, 4 };
private static final int MSG_MOVE_FOCUS = 1000;
- private VerticalGridView mView;
- private MyHandler mHandler = new MyHandler(this);
+ private final VerticalGridView mView;
+ private final MyHandler mHandler = new MyHandler(this);
private int mDirection;
private boolean mFocusAccelerated;
private long mRepeatedKeyInterval;
diff --git a/src/com/android/tv/ui/SelectInputView.java b/src/com/android/tv/ui/SelectInputView.java
index 032782bd..646f9159 100644
--- a/src/com/android/tv/ui/SelectInputView.java
+++ b/src/com/android/tv/ui/SelectInputView.java
@@ -249,14 +249,16 @@ public class SelectInputView extends VerticalGridView implements
boolean foundTuner = false;
for (TvInputInfo input : mTvInputManagerHelper.getTvInputInfos(false, false)) {
if (input.isPassthroughInput()) {
- mInputList.add(input);
- inputMap.put(input.getId(), input);
+ if (!input.isHidden(getContext())) {
+ mInputList.add(input);
+ inputMap.put(input.getId(), input);
+ }
} else if (!foundTuner) {
foundTuner = true;
mInputList.add(input);
}
}
- // Do not show an AVR if an HDMI device is connected to it.
+ // Do not show HDMI ports if a CEC device is directly connected to the port.
for (TvInputInfo input : inputMap.values()) {
if (input.getParentId() != null && !input.isConnectedToHdmiSwitch()) {
mInputList.remove(inputMap.get(input.getParentId()));
diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java
index 286bc1f9..6d3d62aa 100644
--- a/src/com/android/tv/ui/TunableTvView.java
+++ b/src/com/android/tv/ui/TunableTvView.java
@@ -17,11 +17,11 @@
package com.android.tv.ui;
import android.animation.Animator;
-import android.animation.AnimatorInflater;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
+import android.content.ContentUris;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.PlaybackParams;
@@ -29,13 +29,18 @@ import android.media.tv.TvContentRating;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvTrackInfo;
+import android.media.tv.TvView;
import android.media.tv.TvView.OnUnhandledInputEventListener;
import android.media.tv.TvView.TvInputCallback;
+import android.net.ConnectivityManager;
import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+import android.support.v4.os.BuildCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
@@ -46,50 +51,57 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
-import android.widget.TextView;
import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.analytics.DurationTimer;
import com.android.tv.analytics.Tracker;
-import com.android.tv.common.recording.PlaybackTvView;
-import com.android.tv.common.recording.RecordingUtils;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.recording.RecordedProgram;
import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.StreamInfo;
import com.android.tv.data.WatchedHistoryManager;
+import com.android.tv.dvr.DvrDataManager;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.recommendation.NotificationService;
+import com.android.tv.util.NetworkUtils;
import com.android.tv.util.PermissionUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.util.List;
public class TunableTvView extends FrameLayout implements StreamInfo {
private static final boolean DEBUG = false;
private static final String TAG = "TunableTvView";
- public static final String PERMISSION_RECEIVE_INPUT_EVENT =
- "com.android.tv.permission.RECEIVE_INPUT_EVENT";
-
public static final int VIDEO_UNAVAILABLE_REASON_NOT_TUNED = -1;
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({BLOCK_SCREEN_TYPE_NO_UI, BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW, BLOCK_SCREEN_TYPE_NORMAL})
+ public @interface BlockScreenType {}
public static final int BLOCK_SCREEN_TYPE_NO_UI = 0;
public static final int BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW = 1;
public static final int BLOCK_SCREEN_TYPE_NORMAL = 2;
+ private static final String PERMISSION_RECEIVE_INPUT_EVENT =
+ "com.android.tv.permission.RECEIVE_INPUT_EVENT";
+
@Retention(RetentionPolicy.SOURCE)
@IntDef({ TIME_SHIFT_STATE_NONE, TIME_SHIFT_STATE_PLAY, TIME_SHIFT_STATE_PAUSE,
- TIME_SHIFT_STATE_REWIND, TIME_SHIFT_STATE_FAST_FORWARD })
- public @interface TimeShiftState {}
- public static final int TIME_SHIFT_STATE_NONE = 0;
- public static final int TIME_SHIFT_STATE_PLAY = 1;
- public static final int TIME_SHIFT_STATE_PAUSE = 2;
- public static final int TIME_SHIFT_STATE_REWIND = 3;
- public static final int TIME_SHIFT_STATE_FAST_FORWARD = 4;
+ TIME_SHIFT_STATE_REWIND, TIME_SHIFT_STATE_FAST_FORWARD })
+ private @interface TimeShiftState {}
+ private static final int TIME_SHIFT_STATE_NONE = 0;
+ private static final int TIME_SHIFT_STATE_PLAY = 1;
+ private static final int TIME_SHIFT_STATE_PAUSE = 2;
+ private static final int TIME_SHIFT_STATE_REWIND = 3;
+ private static final int TIME_SHIFT_STATE_FAST_FORWARD = 4;
private static final int FADED_IN = 0;
private static final int FADED_OUT = 1;
@@ -103,6 +115,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private AppLayerTvView mTvView;
private Channel mCurrentChannel;
+ private RecordedProgram mRecordedProgram;
private TvInputManagerHelper mInputManagerHelper;
private ContentRatingsManager mContentRatingsManager;
@Nullable
@@ -114,6 +127,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private int mVideoHeight;
private int mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
private float mVideoFrameRate;
+ private float mVideoDisplayAspectRatio;
private int mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
private boolean mHasClosedCaption = false;
private boolean mVideoAvailable;
@@ -130,7 +144,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private boolean mIsPip;
private int mScreenHeight;
private int mShrunkenTvViewHeight;
- private boolean mCanModifyParentalControls;
+ private final boolean mCanModifyParentalControls;
@TimeShiftState private int mTimeShiftState = TIME_SHIFT_STATE_NONE;
private TimeShiftListener mTimeShiftListener;
@@ -139,22 +153,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private final Tracker mTracker;
private final DurationTimer mChannelViewTimer = new DurationTimer();
+ private InternetCheckTask mInternetCheckTask;
// A block screen view which has lock icon with black background.
// This indicates that user's action is needed to play video.
- private final View mBlockScreenView;
-
- private final View mBlockScreenDescriptionView;
- private final ImageView mBlockScreenIconView;
- private final View mBlockScreenShrunkenIconView;
- private final TextView mBlockScreenTextView;
-
- // Animators used for fade in/out of block screen icon.
- private final Animator mBlockScreenDescriptionFadeIn;
- private final Animator mBlockScreenDescriptionFadeOut;
+ private final BlockScreenView mBlockScreenView;
// A View to hide screen when there's problem in video playback.
- private final TextView mHideScreenView;
+ private final BlockScreenView mHideScreenView;
// A View to block screen until onContentAllowed is received if parental control is on.
private final View mBlockScreenForTuneView;
@@ -167,7 +173,11 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private int mFadeState = FADED_IN;
private Runnable mActionAfterFade;
- private int mBlockScreenType;
+ @BlockScreenType private int mBlockScreenType;
+
+ private final DvrDataManager mDvrDataManager;
+ private final ChannelDataManager mChannelDataManager;
+ private final ConnectivityManager mConnectivityManager;
private final TvInputCallback mCallback =
new TvInputCallback() {
@@ -239,6 +249,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
mVideoHeight = 0;
mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
mVideoFrameRate = 0f;
+ mVideoDisplayAspectRatio = 0f;
} else if (type == TvTrackInfo.TYPE_AUDIO) {
mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
}
@@ -254,6 +265,18 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
mVideoFormat = Utils.getVideoDefinitionLevelFromSize(
mVideoWidth, mVideoHeight);
mVideoFrameRate = track.getVideoFrameRate();
+ if (mVideoWidth <= 0 || mVideoHeight <= 0) {
+ mVideoDisplayAspectRatio = 0.0f;
+ } else if (android.os.Build.VERSION.SDK_INT >=
+ android.os.Build.VERSION_CODES.M) {
+ float VideoPixelAspectRatio =
+ track.getVideoPixelAspectRatio();
+ mVideoDisplayAspectRatio = VideoPixelAspectRatio
+ * mVideoWidth / mVideoHeight;
+ } else {
+ mVideoDisplayAspectRatio = mVideoWidth
+ / (float) mVideoHeight;
+ }
} else if (type == TvTrackInfo.TYPE_AUDIO) {
mAudioChannelCount = track.getAudioChannelCount();
}
@@ -315,7 +338,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
@Override
@TargetApi(Build.VERSION_CODES.M)
public void onTimeShiftStatusChanged(String inputId, int status) {
- setTimeShiftAvailable(status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE);
+ boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE;
+ setTimeShiftAvailable(available);
}
};
@@ -336,53 +360,32 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
inflate(getContext(), R.layout.tunable_tv_view, this);
ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
+ mDvrDataManager = CommonFeatures.DVR.isEnabled(context) && BuildCompat.isAtLeastN()
+ ? appSingletons.getDvrDataManager()
+ : null;
+ mChannelDataManager = appSingletons.getChannelDataManager();
+ mConnectivityManager = (ConnectivityManager) context
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
mCanModifyParentalControls = PermissionUtils.hasModifyParentalControls(context);
mTracker = appSingletons.getTracker();
mBlockScreenType = BLOCK_SCREEN_TYPE_NORMAL;
- mBlockScreenView = findViewById(R.id.block_screen);
- mBlockScreenDescriptionView = findViewById(R.id.block_screen_description);
-
- mBlockScreenIconView = (ImageView) mBlockScreenView.findViewById(R.id.block_screen_icon);
+ mBlockScreenView = (BlockScreenView) findViewById(R.id.block_screen);
if (!mCanModifyParentalControls) {
- mBlockScreenIconView.setImageResource(R.drawable.ic_message_lock_no_permission);
- mBlockScreenIconView.setScaleType(ImageView.ScaleType.CENTER);
+ mBlockScreenView.setImage(R.drawable.ic_message_lock_no_permission);
+ mBlockScreenView.setScaleType(ImageView.ScaleType.CENTER);
+ } else {
+ mBlockScreenView.setImage(R.drawable.ic_message_lock);
}
- mBlockScreenShrunkenIconView = mBlockScreenView.findViewById(
- R.id.block_screen_shrunken_icon);
- mBlockScreenTextView = (TextView) mBlockScreenView.findViewById(R.id.block_screen_text);
-
- mBlockScreenDescriptionFadeIn = AnimatorInflater.loadAnimator(context,
- R.animator.tvview_block_screen_fade_in);
- mBlockScreenDescriptionFadeIn.setTarget(mBlockScreenDescriptionView);
- mBlockScreenDescriptionFadeIn.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- switch (mBlockScreenType) {
- case BLOCK_SCREEN_TYPE_NORMAL:
- mBlockScreenIconView.setVisibility(VISIBLE);
- mBlockScreenShrunkenIconView.setVisibility(GONE);
- break;
- case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
- mBlockScreenIconView.setVisibility(GONE);
- mBlockScreenShrunkenIconView.setVisibility(VISIBLE);
- break;
- }
- mBlockScreenDescriptionView.setVisibility(VISIBLE);
- }
- });
- mBlockScreenDescriptionFadeOut = AnimatorInflater.loadAnimator(context,
- R.animator.tvview_block_screen_fade_out);
- mBlockScreenDescriptionFadeOut.setTarget(mBlockScreenDescriptionView);
- mBlockScreenDescriptionFadeOut.addListener(new AnimatorListenerAdapter() {
+ mBlockScreenView.setShrunkenImage(R.drawable.ic_message_lock_preview);
+ mBlockScreenView.addFadeOutAnimationListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- mBlockScreenDescriptionView.setVisibility(GONE);
- mBlockScreenDescriptionView.setAlpha(1f);
- updateBlockScreenTextView();
+ adjustBlockScreenSpacingAndText();
}
});
- mHideScreenView = (TextView) findViewById(R.id.hide_screen);
+ mHideScreenView = (BlockScreenView) findViewById(R.id.hide_screen);
+ mHideScreenView.setImageVisibility(false);
mBufferingSpinnerView = findViewById(R.id.buffering_spinner);
mBlockScreenForTuneView = findViewById(R.id.block_screen_for_tune);
mDimScreenView = findViewById(R.id.dim);
@@ -441,6 +444,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
public void reset() {
mTvView.reset();
mCurrentChannel = null;
+ mRecordedProgram = null;
mInputInfo = null;
mCanReceiveInputEvent = false;
mOnTuneListener = null;
@@ -471,15 +475,82 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
/**
+ * Returns {@code true}, if this view is the recording playback mode.
+ */
+ public boolean isRecordingPlayback() {
+ return mRecordedProgram != null;
+ }
+
+ /**
+ * Returns the recording which is being played right now.
+ */
+ public RecordedProgram getPlayingRecordedProgram() {
+ return mRecordedProgram;
+ }
+
+ /**
* Plays a recording.
*/
- public boolean playRecording(String inputId, Uri recordingUri, OnTuneListener listener) {
- // Create a dummy channel.
- Channel channel = new Channel.Builder()
- .setId(0)
- .setInputId(inputId)
- .build();
- return tuneTo(channel, RecordingUtils.buildMediaUri(recordingUri), listener);
+ public boolean playRecording(Uri recordingUri, OnTuneListener listener) {
+ if (!mStarted) {
+ throw new IllegalStateException("TvView isn't started");
+ }
+ if (!CommonFeatures.DVR.isEnabled(getContext()) || !BuildCompat.isAtLeastN()) {
+ return false;
+ }
+ if (DEBUG) Log.d(TAG, "playRecording " + recordingUri);
+ long recordingId = ContentUris.parseId(recordingUri);
+ mRecordedProgram = mDvrDataManager.getRecordedProgram(recordingId);
+ if (mRecordedProgram == null) {
+ Log.w(TAG, "No recorded program (Uri=" + recordingUri + ")");
+ return false;
+ }
+ String inputId = mRecordedProgram.getInputId();
+ TvInputInfo inputInfo = mInputManagerHelper.getTvInputInfo(inputId);
+ if (inputInfo == null) {
+ return false;
+ }
+ mOnTuneListener = listener;
+ // mCurrentChannel can be null.
+ mCurrentChannel = mChannelDataManager.getChannel(mRecordedProgram.getChannelId());
+ // For recording playback, input event should not be sent.
+ mCanReceiveInputEvent = false;
+ boolean needSurfaceSizeUpdate = false;
+ if (!inputInfo.equals(mInputInfo)) {
+ mInputInfo = inputInfo;
+ if (DEBUG) {
+ Log.d(TAG, "Input \'" + mInputInfo.getId() + "\' can receive input event: "
+ + mCanReceiveInputEvent);
+ }
+ needSurfaceSizeUpdate = true;
+ }
+ mChannelViewTimer.start();
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+ mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
+ mVideoFrameRate = 0f;
+ mVideoDisplayAspectRatio = 0f;
+ mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
+ mHasClosedCaption = false;
+ mTvView.setCallback(mCallback);
+ mTimeShiftCurrentPositionMs = INVALID_TIME;
+ mTvView.setTimeShiftPositionCallback(null);
+ setTimeShiftAvailable(false);
+ mTvView.timeShiftPlay(inputId, recordingUri);
+ if (needSurfaceSizeUpdate && mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) {
+ // When the input is changed, TvView recreates its SurfaceView internally.
+ // So we need to call SurfaceHolder.setFixedSize for the new SurfaceView.
+ getSurfaceView().getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight);
+ }
+ hideScreenByVideoAvailability(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
+ unblockScreenByContentRating();
+ if (mParentControlEnabled) {
+ mBlockScreenForTuneView.setVisibility(View.VISIBLE);
+ }
+ if (mOnTuneListener != null) {
+ mOnTuneListener.onStreamInfoChanged(this);
+ }
+ return true;
}
/**
@@ -508,6 +579,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
mOnTuneListener = listener;
mCurrentChannel = channel;
+ mRecordedProgram = null;
boolean tunedByRecommendation = params != null
&& params.getString(NotificationService.TUNE_PARAMS_RECOMMENDATION_TYPE) != null;
boolean needSurfaceSizeUpdate = false;
@@ -528,6 +600,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
mVideoHeight = 0;
mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
mVideoFrameRate = 0f;
+ mVideoDisplayAspectRatio = 0f;
mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
mHasClosedCaption = false;
mTvView.setCallback(mCallback);
@@ -590,8 +663,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
* Note: Once {@link android.view.SurfaceHolder#setFixedSize} is called,
* {@link android.view.SurfaceView} and its underlying window can be misaligned, when the size
* of {@link android.view.SurfaceView} is changed without changing either left position or top
- * position. For detail, please refer the codes of {@link android.view.SurfaceView#updateWindow}
- * .
+ * position. For detail, please refer the codes of android.view.SurfaceView.updateWindow().
*/
public void setFixedSurfaceSize(int width, int height) {
mFixedSurfaceWidth = width;
@@ -636,8 +708,18 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
void onContentAllowed();
}
- public void requestUnblockContent(TvContentRating rating) {
- mTvView.unblockContent(rating);
+ public void unblockContent(TvContentRating rating) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ try {
+ Method method = TvView.class.getMethod("requestUnblockContent",
+ TvContentRating.class);
+ method.invoke(mTvView, rating);
+ } catch (NoSuchMethodException|IllegalAccessException|InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ } else {
+ mTvView.unblockContent(rating);
+ }
}
@Override
@@ -660,6 +742,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
return mVideoFrameRate;
}
+ /**
+ * Returns displayed aspect ratio (video width / video height * pixel ratio).
+ */
+ @Override
+ public float getVideoDisplayAspectRatio() {
+ return mVideoDisplayAspectRatio;
+ }
+
@Override
public int getAudioChannelCount() {
return mAudioChannelCount;
@@ -683,7 +773,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
/**
* Returns the {@link android.view.SurfaceView} of the {@link android.media.tv.TvView}.
*/
- public SurfaceView getSurfaceView() {
+ private SurfaceView getSurfaceView() {
return (SurfaceView) mTvView.getChildAt(0);
}
@@ -757,8 +847,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
scale = height * PIP_BLOCK_SCREEN_SCALE_FACTOR / mScreenHeight;
}
// TODO: need to get UX confirmation.
- mBlockScreenDescriptionView.setScaleX(scale);
- mBlockScreenDescriptionView.setScaleY(scale);
+ mBlockScreenView.scaleContainerView(scale);
}
}
@@ -817,7 +906,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
*
* @param type The type of block screen to set.
*/
- public void setBlockScreenType(int type) {
+ public void setBlockScreenType(@BlockScreenType int type) {
// TODO: need to support the transition from NORMAL to SHRUNKEN and vice verse.
if (mBlockScreenType != type) {
mBlockScreenType = type;
@@ -826,12 +915,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
private void updateBlockScreenUI(boolean animation) {
- if (mBlockScreenDescriptionFadeIn.isRunning()) {
- mBlockScreenDescriptionFadeIn.end();
- }
- if (mBlockScreenDescriptionFadeOut.isRunning()) {
- mBlockScreenDescriptionFadeOut.end();
- }
+ mBlockScreenView.endAnimations();
if (!mScreenBlocked && mBlockedContentRating == null) {
mBlockScreenView.setVisibility(GONE);
@@ -839,101 +923,72 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
mBlockScreenView.setVisibility(VISIBLE);
- if (!animation) {
- updateBlockScreenTextView();
- switch (mBlockScreenType) {
- case BLOCK_SCREEN_TYPE_NO_UI:
- mBlockScreenIconView.setVisibility(GONE);
- mBlockScreenShrunkenIconView.setVisibility(GONE);
- mBlockScreenDescriptionView.setVisibility(GONE);
- break;
- case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
- mBlockScreenIconView.setVisibility(GONE);
- mBlockScreenShrunkenIconView.setVisibility(VISIBLE);
- mBlockScreenDescriptionView.setVisibility(VISIBLE);
- break;
- case BLOCK_SCREEN_TYPE_NORMAL:
- mBlockScreenIconView.setVisibility(VISIBLE);
- mBlockScreenShrunkenIconView.setVisibility(GONE);
- mBlockScreenDescriptionView.setVisibility(VISIBLE);
- break;
- }
- } else {
- switch (mBlockScreenType) {
- case BLOCK_SCREEN_TYPE_NO_UI:
- if (mBlockScreenDescriptionView.getVisibility() == VISIBLE) {
- mBlockScreenDescriptionFadeOut.start();
- }
- break;
- case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
- case BLOCK_SCREEN_TYPE_NORMAL:
- updateBlockScreenTextView();
- if (mBlockScreenDescriptionView.getVisibility() == GONE) {
- mBlockScreenDescriptionFadeIn.start();
- }
- break;
- }
+ if (!animation || mBlockScreenType != TunableTvView.BLOCK_SCREEN_TYPE_NO_UI) {
+ adjustBlockScreenSpacingAndText();
}
+ mBlockScreenView.onBlockStatusChanged(mBlockScreenType, animation);
}
- private void updateBlockScreenTextView() {
+ private void adjustBlockScreenSpacingAndText() {
// TODO: need to add animation for padding change when the block screen type is changed
// NORMAL to SHRUNKEN and vice verse.
- mBlockScreenTextView.setPadding(0,
- getResources().getDimensionPixelOffset(
- mBlockScreenType == BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW
- ? R.dimen.shrunken_tvview_block_text_padding_top
- : R.dimen.tvview_block_text_padding_top),
- 0, 0);
+ mBlockScreenView.setSpacing(mBlockScreenType);
+ String text = getBlockScreenText();
+ if (text != null) {
+ mBlockScreenView.setText(text);
+ }
+ }
+ /**
+ * Returns the block screen text corresponding to the current status.
+ * Note that returning {@code null} value means that the current text should not be changed.
+ */
+ private String getBlockScreenText() {
if (mScreenBlocked) {
switch (mBlockScreenType) {
case BLOCK_SCREEN_TYPE_NO_UI:
case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
- mBlockScreenTextView.setText("");
- break;
+ return "";
case BLOCK_SCREEN_TYPE_NORMAL:
if (mCanModifyParentalControls) {
- mBlockScreenTextView.setText(R.string.tvview_channel_locked);
+ return getResources().getString(R.string.tvview_channel_locked);
} else {
- mBlockScreenTextView.setText(R.string.tvview_channel_locked_no_permission);
+ return getResources().getString(
+ R.string.tvview_channel_locked_no_permission);
}
- break;
}
} else if (mBlockedContentRating != null) {
String name = mContentRatingsManager.getDisplayNameForRating(mBlockedContentRating);
switch (mBlockScreenType) {
case BLOCK_SCREEN_TYPE_NO_UI:
- mBlockScreenTextView.setText("");
- break;
+ return "";
case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
if (TextUtils.isEmpty(name)) {
- mBlockScreenTextView.setText(R.string.shrunken_tvview_content_locked);
+ return getResources().getString(R.string.shrunken_tvview_content_locked);
} else {
- mBlockScreenTextView.setText(getContext().getString(
- R.string.shrunken_tvview_content_locked_format, name));
+ return getContext().getString(
+ R.string.shrunken_tvview_content_locked_format, name);
}
- break;
case BLOCK_SCREEN_TYPE_NORMAL:
if (TextUtils.isEmpty(name)) {
if (mCanModifyParentalControls) {
- mBlockScreenTextView.setText(R.string.tvview_content_locked);
+ return getResources().getString(R.string.tvview_content_locked);
} else {
- mBlockScreenTextView.setText(
+ return getResources().getString(
R.string.tvview_content_locked_no_permission);
}
} else {
if (mCanModifyParentalControls) {
- mBlockScreenTextView.setText(getContext().getString(
- R.string.tvview_content_locked_format, name));
+ return getContext().getString(
+ R.string.tvview_content_locked_format, name);
} else {
- mBlockScreenTextView.setText(getContext().getString(
- R.string.tvview_content_locked_format_no_permission, name));
+ return getContext().getString(
+ R.string.tvview_content_locked_format_no_permission, name);
}
}
- break;
}
}
+ return null;
}
private void checkBlockScreenAndMuteNeeded() {
@@ -968,10 +1023,18 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
checkBlockScreenAndMuteNeeded();
}
+ @UiThread
private void hideScreenByVideoAvailability(int reason) {
+ mVideoAvailable = false;
+ mVideoUnavailableReason = reason;
+ if (mInternetCheckTask != null) {
+ mInternetCheckTask.cancel(true);
+ mInternetCheckTask = null;
+ }
switch (reason) {
case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
mHideScreenView.setVisibility(VISIBLE);
+ mHideScreenView.setImageVisibility(false);
mHideScreenView.setText(R.string.tvview_msg_audio_only);
mBufferingSpinnerView.setVisibility(GONE);
unmuteIfPossible();
@@ -980,19 +1043,33 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
mBufferingSpinnerView.setVisibility(VISIBLE);
mute();
break;
- case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
- case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
+ mHideScreenView.setVisibility(VISIBLE);
+ mHideScreenView.setText(R.string.tvview_msg_weak_signal);
+ mBufferingSpinnerView.setVisibility(GONE);
+ mute();
+ break;
+ case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING:
case VIDEO_UNAVAILABLE_REASON_NOT_TUNED:
+ mHideScreenView.setVisibility(VISIBLE);
+ mHideScreenView.setImageVisibility(false);
+ mHideScreenView.setText(null);
+ mBufferingSpinnerView.setVisibility(GONE);
+ mute();
+ break;
+ case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
default:
mHideScreenView.setVisibility(VISIBLE);
+ mHideScreenView.setImageVisibility(false);
mHideScreenView.setText(null);
mBufferingSpinnerView.setVisibility(GONE);
mute();
+ if (mCurrentChannel != null && !mCurrentChannel.isPhysicalTunerChannel()) {
+ mInternetCheckTask = new InternetCheckTask();
+ mInternetCheckTask.execute();
+ }
break;
}
- mVideoAvailable = false;
- mVideoUnavailableReason = reason;
}
private void unhideScreenByVideoAvailability() {
@@ -1094,7 +1171,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
mTimeShiftAvailable = isTimeShiftAvailable;
if (isTimeShiftAvailable) {
- mTvView.setTimeShiftPositionCallback(new PlaybackTvView.TimeShiftPositionCallback2() {
+ mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() {
@Override
public void onTimeShiftStartPositionChanged(String inputId, long timeMs) {
if (mTimeShiftListener != null && mCurrentChannel != null
@@ -1107,14 +1184,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) {
mTimeShiftCurrentPositionMs = timeMs;
}
-
- @Override
- public void onTimeShiftEndPositionChanged(String inputId, long timeMs) {
- if (mTimeShiftListener != null && mCurrentChannel != null
- && mCurrentChannel.getInputId().equals(inputId)) {
- mTimeShiftListener.onRecordEndTimeChanged(timeMs);
- }
- }
});
} else {
mTvView.setTimeShiftPositionCallback(null);
@@ -1265,11 +1334,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
* Called when the record start time has been changed.
*/
public abstract void onRecordStartTimeChanged(long recordStartTimeMs);
-
- /**
- * Called when the record end time has been changed.
- */
- public abstract void onRecordEndTimeChanged(long recordEndTimeMs);
}
/**
@@ -1281,4 +1345,22 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
*/
public abstract void onScreenBlockingChanged(boolean blocked);
}
+
+ public class InternetCheckTask extends AsyncTask<Void, Void, Boolean> {
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ return NetworkUtils.isNetworkAvailable(mConnectivityManager);
+ }
+
+ @Override
+ protected void onPostExecute(Boolean networkAvailable) {
+ mInternetCheckTask = null;
+ if (!mVideoAvailable && !networkAvailable && isAttachedToWindow()
+ && mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN) {
+ mHideScreenView.setImageVisibility(true);
+ mHideScreenView.setImage(R.drawable.ic_sad_cloud);
+ mHideScreenView.setText(R.string.tvview_msg_no_internet_connection);
+ }
+ }
+ }
}
diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java
index 124f3393..94f9b0f9 100644
--- a/src/com/android/tv/ui/TvOverlayManager.java
+++ b/src/com/android/tv/ui/TvOverlayManager.java
@@ -29,6 +29,7 @@ import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
+import android.support.v4.os.BuildCompat;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
@@ -46,6 +47,7 @@ import com.android.tv.TimeShiftManager;
import com.android.tv.TvApplication;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.WeakHandler;
+import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.ui.setup.OnActionClickListener;
import com.android.tv.common.ui.setup.SetupFragment;
import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
@@ -54,7 +56,9 @@ import com.android.tv.dialog.FullscreenDialogFragment;
import com.android.tv.dialog.PinDialogFragment;
import com.android.tv.dialog.RecentlyWatchedDialogFragment;
import com.android.tv.dialog.SafeDismissDialogFragment;
+import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.ui.DvrActivity;
+import com.android.tv.dvr.ui.HalfSizedDialogFragment;
import com.android.tv.guide.ProgramGuide;
import com.android.tv.menu.Menu;
import com.android.tv.menu.Menu.MenuShowReason;
@@ -133,6 +137,7 @@ public class TvOverlayManager {
AVAILABLE_DIALOG_TAGS.add(FullscreenDialogFragment.DIALOG_TAG);
AVAILABLE_DIALOG_TAGS.add(SettingsFragment.LicenseActionItem.DIALOG_TAG);
AVAILABLE_DIALOG_TAGS.add(RatingsFragment.AttributionItem.DIALOG_TAG);
+ AVAILABLE_DIALOG_TAGS.add(HalfSizedDialogFragment.DIALOG_TAG);
}
private final MainActivity mMainActivity;
@@ -155,7 +160,7 @@ public class TvOverlayManager {
private @TvOverlayType int mOpenedOverlays;
- private List<Runnable> mPendingActions = new ArrayList<>();
+ private final List<Runnable> mPendingActions = new ArrayList<>();
public TvOverlayManager(MainActivity mainActivity, ChannelTuner channelTuner,
KeypadChannelSwitchView keypadChannelSwitchView,
@@ -227,9 +232,13 @@ public class TvOverlayManager {
onOverlayClosed(OVERLAY_TYPE_GUIDE);
}
};
+ DvrDataManager dvrDataManager =
+ CommonFeatures.DVR.isEnabled(mainActivity) && BuildCompat.isAtLeastN() ? singletons
+ .getDvrDataManager() : null;
mProgramGuide = new ProgramGuide(mainActivity, channelTuner,
singletons.getTvInputManagerHelper(), mChannelDataManager,
- singletons.getProgramDataManager(), singletons.getTracker(), preShowRunnable,
+ singletons.getProgramDataManager(), dvrDataManager, singletons.getTracker(),
+ preShowRunnable,
postHideRunnable);
mSetupFragment = new SetupSourcesFragment();
mSetupFragment.setOnActionClickListener(new OnActionClickListener() {
@@ -346,10 +355,18 @@ public class TvOverlayManager {
*/
public void showDialogFragment(String tag, SafeDismissDialogFragment dialog,
boolean keepSidePanelHistory) {
+ showDialogFragment(tag, dialog, keepSidePanelHistory, false);
+ }
+
+ public void showDialogFragment(String tag, SafeDismissDialogFragment dialog,
+ boolean keepSidePanelHistory, boolean keepProgramGuide) {
int flags = FLAG_HIDE_OVERLAYS_KEEP_DIALOG;
if (keepSidePanelHistory) {
flags |= FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY;
}
+ if (keepProgramGuide) {
+ flags |= FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE;
+ }
hideOverlays(flags);
// A tag for dialog must be added to AVAILABLE_DIALOG_TAGS to make it launchable from TV.
if (!AVAILABLE_DIALOG_TAGS.contains(tag)) {
@@ -422,7 +439,6 @@ public class TvOverlayManager {
public void showSetupFragment() {
if (DEBUG) Log.d(TAG, "showSetupFragment");
mSetupFragmentActive = true;
- SetupSourcesFragment.setTheme(R.style.Theme_TV_GuidedStep);
mSetupFragment.enableFragmentTransition(SetupFragment.FRAGMENT_ENTER_TRANSITION
| SetupFragment.FRAGMENT_EXIT_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION
| SetupFragment.FRAGMENT_REENTER_TRANSITION);
@@ -437,7 +453,6 @@ public class TvOverlayManager {
return;
}
mSetupFragmentActive = false;
- SetupSourcesFragment.setTheme(SetupSourcesFragment.DEFAULT_THEME);
closeFragment(removeFragment ? mSetupFragment : null);
if (mChannelDataManager.getChannelCount() == 0) {
mMainActivity.finish();
diff --git a/src/com/android/tv/ui/TvTransitionManager.java b/src/com/android/tv/ui/TvTransitionManager.java
index 444b5c0c..52e96cc0 100644
--- a/src/com/android/tv/ui/TvTransitionManager.java
+++ b/src/com/android/tv/ui/TvTransitionManager.java
@@ -33,6 +33,7 @@ import android.widget.FrameLayout.LayoutParams;
import com.android.tv.MainActivity;
import com.android.tv.R;
+import com.android.tv.data.Channel;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -89,15 +90,21 @@ public class TvTransitionManager extends TransitionManager {
}
initIfNeeded();
if (withAnimation) {
+ mEmptyView.setAlpha(1.0f);
transitionTo(mEmptyScene);
} else {
TransitionManager.go(mEmptyScene, null);
+ // When transition is null, transition got stuck without calling endTransitions.
+ TransitionManager.endTransitions(mEmptyScene.getSceneRoot());
+ // Since Fade.OUT transition doesn't run, we need to set alpha manually.
+ mEmptyView.setAlpha(0);
}
}
public void goToChannelBannerScene() {
initIfNeeded();
- if (mMainActivity.getCurrentChannel().isPassthrough()) {
+ Channel channel = mMainActivity.getCurrentChannel();
+ if (channel != null && channel.isPassthrough()) {
if (mCurrentScene != mInputBannerScene) {
// Show the input banner instead.
LayoutParams lp = (LayoutParams) mInputBannerView.getLayoutParams();
@@ -152,7 +159,7 @@ public class TvTransitionManager extends TransitionManager {
mExitAnimator = AnimatorInflater.loadAnimator(mMainActivity,
R.animator.channel_banner_exit);
- mEmptyScene = new Scene(mSceneContainer, mEmptyView);
+ mEmptyScene = new Scene(mSceneContainer, (View) mEmptyView);
mEmptyScene.setEnterAction(new Runnable() {
@Override
public void run() {
diff --git a/src/com/android/tv/ui/TvViewUiManager.java b/src/com/android/tv/ui/TvViewUiManager.java
index d767906b..5ad89bfa 100644
--- a/src/com/android/tv/ui/TvViewUiManager.java
+++ b/src/com/android/tv/ui/TvViewUiManager.java
@@ -59,6 +59,7 @@ public class TvViewUiManager {
private static final boolean DEBUG = false;
private static final float DISPLAY_MODE_EPSILON = 0.001f;
+ private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f;
private final Context mContext;
private final Resources mResources;
@@ -71,8 +72,8 @@ public class TvViewUiManager {
private final int mTvViewShrunkenEndMargin;
private final int mTvViewPapStartMargin;
private final int mTvViewPapEndMargin;
- private final int mScreenWidth;
- private final int mScreenHeight;
+ private int mWindowWidth;
+ private int mWindowHeight;
private final int mPipViewHorizontalMargin;
private final int mPipViewTopMargin;
private final int mPipViewBottomMargin;
@@ -101,28 +102,28 @@ public class TvViewUiManager {
private ObjectAnimator mBackgroundAnimator;
private int mBackgroundColor;
private int mAppliedDisplayedMode = DisplayMode.MODE_NOT_DEFINED;
- private int mAppliedVideoWidth;
- private int mAppliedVideoHeight;
private int mAppliedTvViewStartMargin;
private int mAppliedTvViewEndMargin;
+ private float mAppliedVideoDisplayAspectRatio;
public TvViewUiManager(Context context, TunableTvView tvView, TunableTvView pipView,
FrameLayout contentView, TvOptionsManager tvOptionManager) {
mContext = context;
- mResources = context.getResources();
+ mResources = mContext.getResources();
mTvView = tvView;
mPipView = pipView;
mContentView = contentView;
mTvOptionsManager = tvOptionManager;
- DisplayManager displayManager = (DisplayManager) context
+ DisplayManager displayManager = (DisplayManager) mContext
.getSystemService(Context.DISPLAY_SERVICE);
Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
Point size = new Point();
display.getSize(size);
- mScreenWidth = size.x;
- mScreenHeight = size.y;
+ mWindowWidth = size.x;
+ mWindowHeight = size.y;
+ // Have an assumption that PIP and TvView Shrinking happens only in full screen.
mTvViewShrunkenStartMargin = mResources
.getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_start);
mTvViewShrunkenEndMargin =
@@ -131,7 +132,7 @@ public class TvViewUiManager {
int papMarginHorizontal = mResources
.getDimensionPixelOffset(R.dimen.papview_margin_horizontal);
int papSpacing = mResources.getDimensionPixelOffset(R.dimen.papview_spacing);
- mTvViewPapWidth = (mScreenWidth - papSpacing) / 2 - papMarginHorizontal;
+ mTvViewPapWidth = (mWindowWidth - papSpacing) / 2 - papMarginHorizontal;
mTvViewPapStartMargin = papMarginHorizontal + mTvViewPapWidth + papSpacing;
mTvViewPapEndMargin = papMarginHorizontal;
mTvViewFrame = createMarginLayoutParams(0, 0, 0, 0);
@@ -147,6 +148,21 @@ public class TvViewUiManager {
.getDimensionPixelOffset(R.dimen.pipview_margin_horizontal);
mPipViewTopMargin = mResources.getDimensionPixelOffset(R.dimen.pipview_margin_top);
mPipViewBottomMargin = mResources.getDimensionPixelOffset(R.dimen.pipview_margin_bottom);
+ mContentView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ int windowWidth = right - left;
+ int windowHeight = bottom - top;
+ if (windowWidth > 0 && windowHeight > 0) {
+ if (mWindowWidth != windowWidth || mWindowHeight != windowHeight) {
+ mWindowWidth = windowWidth;
+ mWindowHeight = windowHeight;
+ applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), false, true);
+ }
+ }
+ }
+ });
}
/**
@@ -170,7 +186,7 @@ public class TvViewUiManager {
mTvViewEndMarginBeforeShrunken = mTvViewEndMargin;
if (mPipStarted && getPipLayout() == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) {
float sidePanelWidth = mResources.getDimensionPixelOffset(R.dimen.side_panel_width);
- float factor = 1.0f - sidePanelWidth / mScreenWidth;
+ float factor = 1.0f - sidePanelWidth / mWindowWidth;
int startMargin = (int) (mTvViewPapStartMargin * factor);
int endMargin = (int) (mTvViewPapEndMargin * factor + sidePanelWidth);
setTvViewMargin(startMargin, endMargin);
@@ -209,25 +225,21 @@ public class TvViewUiManager {
int viewWidth = mContentView.getWidth();
int viewHeight = mContentView.getHeight();
- int videoWidth = mTvView.getVideoWidth();
- int videoHeight = mTvView.getVideoHeight();
-
- if (viewWidth <= 0 || viewHeight <= 0 || videoWidth <= 0 || videoHeight <= 0) {
+ float videoDisplayAspectRatio = mTvView.getVideoDisplayAspectRatio();
+ if (viewWidth <= 0 || viewHeight <= 0 || videoDisplayAspectRatio <= 0f) {
Log.w(TAG, "Video size is currently unavailable");
if (DEBUG) {
Log.d(TAG, "isDisplayModeAvailable: "
+ "viewWidth=" + viewWidth
+ ", viewHeight=" + viewHeight
- + ", videoWidth=" + videoWidth
- + ", videoHeight="+ videoHeight
+ + ", videoDisplayAspectRatio=" + videoDisplayAspectRatio
);
}
return false;
}
float viewRatio = viewWidth / (float) viewHeight;
- float videoRatio = videoWidth / (float) videoHeight;
- return Math.abs(viewRatio - videoRatio) >= DISPLAY_MODE_EPSILON;
+ return Math.abs(viewRatio - videoDisplayAspectRatio) >= DISPLAY_MODE_EPSILON;
}
/**
@@ -251,7 +263,7 @@ public class TvViewUiManager {
if (storeInPreference) {
mSharedPreferences.edit().putInt(TvSettings.PREF_DISPLAY_MODE, displayMode).apply();
}
- applyDisplayMode(mTvView.getVideoWidth(), mTvView.getVideoHeight(), animate);
+ applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), animate, false);
return prev;
}
@@ -268,7 +280,7 @@ public class TvViewUiManager {
* Updates TvView. It is called when video resolution is updated.
*/
public void updateTvView() {
- applyDisplayMode(mTvView.getVideoWidth(), mTvView.getVideoHeight(), false);
+ applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), false, false);
if (mTvView.isVideoAvailable() && mTvView.isFadedOut()) {
mTvView.fadeIn(mResources.getInteger(R.integer.tvview_fade_in_duration),
mFastOutLinearIn, null);
@@ -507,11 +519,11 @@ public class TvViewUiManager {
@Override
public void run() {
if (DEBUG) {
- Log.d(TAG, "setFixedSize: w=" + mTvView.getWidth() + " h=" + mTvView
- .getHeight());
+ Log.d(TAG, "setFixedSize: w=" + layoutParams.width + " h="
+ + layoutParams.height);
}
mTvView.setLayoutParams(layoutParams);
- mTvView.setFixedSurfaceSize(mTvView.getWidth(), mTvView.getHeight());
+ mTvView.setFixedSurfaceSize(layoutParams.width, layoutParams.height);
}
});
} else {
@@ -541,15 +553,14 @@ public class TvViewUiManager {
if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) {
gravity = Gravity.CENTER_VERTICAL | Gravity.START;
height = tvViewFrame.height;
- int pipVideoWidth = mPipView.getVideoWidth();
- int pipVideoHeight = mPipView.getVideoHeight();
- if (pipVideoWidth > 0 && pipVideoHeight > 0) {
- width = height * pipVideoWidth / pipVideoHeight;
+ float videoDisplayAspectRatio = mPipView.getVideoDisplayAspectRatio();
+ if (videoDisplayAspectRatio <= 0f) {
+ width = tvViewFrame.width;
+ } else {
+ width = (int) (height * videoDisplayAspectRatio);
if (width > tvViewFrame.width) {
width = tvViewFrame.width;
}
- } else {
- width = tvViewFrame.width;
}
startMargin = mResources.getDimensionPixelOffset(R.dimen.papview_margin_horizontal)
* tvViewFrame.width / mTvViewPapWidth + (tvViewFrame.width - width) / 2;
@@ -563,8 +574,8 @@ public class TvViewUiManager {
int tvEndMargin = tvViewFrame.getMarginEnd();
int tvTopMargin = tvViewFrame.topMargin;
int tvBottomMargin = tvViewFrame.bottomMargin;
- float horizontalScaleFactor = (float) tvViewWidth / mScreenWidth;
- float verticalScaleFactor = (float) tvViewHeight / mScreenHeight;
+ float horizontalScaleFactor = (float) tvViewWidth / mWindowWidth;
+ float verticalScaleFactor = (float) tvViewHeight / mWindowHeight;
int maxWidth;
if (mPipSize == TvSettings.PIP_SIZE_SMALL) {
@@ -580,15 +591,14 @@ public class TvViewUiManager {
} else {
throw new IllegalArgumentException("Invalid PIP size: " + mPipSize);
}
- int pipVideoWidth = mPipView.getVideoWidth();
- int pipVideoHeight = mPipView.getVideoHeight();
- if (pipVideoWidth > 0 && pipVideoHeight > 0) {
- width = height * pipVideoWidth / pipVideoHeight;
+ float videoDisplayAspectRatio = mPipView.getVideoDisplayAspectRatio();
+ if (videoDisplayAspectRatio <= 0f) {
+ width = maxWidth;
+ } else {
+ width = (int) (height * videoDisplayAspectRatio);
if (width > maxWidth) {
width = maxWidth;
}
- } else {
- width = maxWidth;
}
startMargin = tvStartMargin + (int) (mPipViewHorizontalMargin * horizontalScaleFactor);
@@ -703,31 +713,29 @@ public class TvViewUiManager {
});
}
- private void applyDisplayMode(int videoWidth, int videoHeight, boolean animate) {
+ private void applyDisplayMode(float videoDisplayAspectRatio, boolean animate,
+ boolean forceUpdate) {
if (mAppliedDisplayedMode == mDisplayMode
- && mAppliedVideoWidth == videoWidth
- && mAppliedVideoHeight == videoHeight
&& mAppliedTvViewStartMargin == mTvViewStartMargin
- && mAppliedTvViewEndMargin == mTvViewEndMargin) {
- return;
+ && mAppliedTvViewEndMargin == mTvViewEndMargin
+ && Math.abs(mAppliedVideoDisplayAspectRatio - videoDisplayAspectRatio) <
+ DISPLAY_ASPECT_RATIO_EPSILON) {
+ if (!forceUpdate) {
+ return;
+ }
} else {
mAppliedDisplayedMode = mDisplayMode;
- mAppliedVideoHeight = videoHeight;
- mAppliedVideoWidth = videoWidth;
mAppliedTvViewStartMargin = mTvViewStartMargin;
mAppliedTvViewEndMargin = mTvViewEndMargin;
+ mAppliedVideoDisplayAspectRatio = videoDisplayAspectRatio;
}
- int availableAreaWidth = mScreenWidth - mTvViewStartMargin - mTvViewEndMargin;
- int availableAreaHeight = availableAreaWidth * mScreenHeight / mScreenWidth;
+ int availableAreaWidth = mWindowWidth - mTvViewStartMargin - mTvViewEndMargin;
+ int availableAreaHeight = availableAreaWidth * mWindowHeight / mWindowWidth;
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(0, 0,
((FrameLayout.LayoutParams) mTvView.getLayoutParams()).gravity);
int displayMode = mDisplayMode;
double availableAreaRatio = 0;
double videoRatio = 0;
- if (videoWidth <= 0 || videoHeight <= 0) {
- videoWidth = mScreenWidth;
- videoHeight = mScreenHeight;
- }
if (availableAreaWidth <= 0 || availableAreaHeight <= 0) {
displayMode = DisplayMode.MODE_FULL;
Log.w(TAG, "Some resolution info is missing during applyDisplayMode. ("
@@ -735,10 +743,14 @@ public class TvViewUiManager {
+ availableAreaHeight + ")");
} else {
availableAreaRatio = (double) availableAreaWidth / availableAreaHeight;
- videoRatio = (double) videoWidth / videoHeight;
+ if (videoDisplayAspectRatio <= 0f) {
+ videoRatio = (double) mWindowWidth / mWindowHeight;
+ } else {
+ videoRatio = videoDisplayAspectRatio;
+ }
}
- int tvViewFrameTop = (mScreenHeight - availableAreaHeight) / 2;
+ int tvViewFrameTop = (mWindowHeight - availableAreaHeight) / 2;
MarginLayoutParams tvViewFrame = createMarginLayoutParams(
mTvViewStartMargin, mTvViewEndMargin, tvViewFrameTop, tvViewFrameTop);
layoutParams.width = availableAreaWidth;
@@ -777,7 +789,7 @@ public class TvViewUiManager {
int marginStart = mTvViewStartMargin + (availableAreaWidth - layoutParams.width) / 2;
layoutParams.setMarginStart(marginStart);
// Set marginEnd as well because setTvViewPosition uses both start/end margin.
- layoutParams.setMarginEnd(mScreenWidth - layoutParams.width - marginStart);
+ layoutParams.setMarginEnd(mWindowWidth - layoutParams.width - marginStart);
setBackgroundColor(Utils.getColor(mResources, isTvViewFullScreen()
? R.color.tvactivity_background : R.color.tvactivity_background_on_shrunken_tvview),
@@ -810,8 +822,8 @@ public class TvViewUiManager {
lp.setMarginEnd(endMargin);
lp.topMargin = topMargin;
lp.bottomMargin = bottomMargin;
- lp.width = mScreenWidth - startMargin - endMargin;
- lp.height = mScreenHeight - topMargin - bottomMargin;
+ lp.width = mWindowWidth - startMargin - endMargin;
+ lp.height = mWindowHeight - topMargin - bottomMargin;
return lp;
}
}
diff --git a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
index 85050dc4..b52302b6 100644
--- a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
+++ b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
@@ -37,6 +37,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.Iterator;
public class CustomizeChannelListFragment extends SideFragment {
private static final int GROUP_BY_SOURCE = 0;
@@ -157,6 +158,21 @@ public class CustomizeChannelListFragment extends SideFragment {
return mItems;
}
+ private void cleanUpOneChannelGroupItem(List<Item> items) {
+ Iterator<Item> iter = items.iterator();
+ while (iter.hasNext()) {
+ Item item = iter.next();
+ if (item instanceof SelectGroupItem) {
+ SelectGroupItem selectGroupItem = (SelectGroupItem) item;
+ if (selectGroupItem.mChannelItemsInGroup.size() == 1) {
+ ((ChannelItem) selectGroupItem.mChannelItemsInGroup.get(0))
+ .mSelectGroupItem = null;
+ iter.remove();
+ }
+ }
+ }
+ }
+
private void addItemForGroupBySource(List<Item> items) {
items.add(new GroupBySubMenu(getString(R.string.edit_channels_group_by_sources)));
SelectGroupItem selectGroupItem = null;
@@ -177,6 +193,7 @@ public class CustomizeChannelListFragment extends SideFragment {
items.add(channelItem);
selectGroupItem.addChannelItem(channelItem);
}
+ cleanUpOneChannelGroupItem(items);
}
private void addItemForGroupByHdSd(List<Item> items) {
@@ -211,6 +228,7 @@ public class CustomizeChannelListFragment extends SideFragment {
items.add(channelItem);
selectGroupItem.addChannelItem(channelItem);
}
+ cleanUpOneChannelGroupItem(items);
}
private static boolean isHdChannel(Channel channel) {
@@ -275,7 +293,7 @@ public class CustomizeChannelListFragment extends SideFragment {
}
private class ChannelItem extends ChannelCheckItem {
- private final SelectGroupItem mSelectGroupItem;
+ private SelectGroupItem mSelectGroupItem;
public ChannelItem(Channel channel, SelectGroupItem selectGroupItem) {
super(channel, getChannelDataManager(), getProgramDataManager());
@@ -292,7 +310,9 @@ public class CustomizeChannelListFragment extends SideFragment {
protected void onSelected() {
super.onSelected();
getChannelDataManager().updateBrowsable(getChannel().getId(), isChecked());
- mSelectGroupItem.notifyUpdated();
+ if (mSelectGroupItem != null) {
+ mSelectGroupItem.notifyUpdated();
+ }
}
@Override
diff --git a/src/com/android/tv/ui/sidepanel/DeveloperFragment.java b/src/com/android/tv/ui/sidepanel/DeveloperFragment.java
deleted file mode 100644
index 44b4d452..00000000
--- a/src/com/android/tv/ui/sidepanel/DeveloperFragment.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.ui.sidepanel;
-
-import android.app.Activity;
-import android.content.Context;
-import android.view.View;
-
-import com.android.tv.Features;
-import com.android.tv.R;
-
-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);
- }
- }
-
- @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));
- boolean ac3Support = getMainActivity().isAc3PassthroughSupported();
- // Show AC3 passthrough availability.
- items.add(new SimpleItem(getString(R.string.developer_menu_ac3_support),
- getString(ac3Support ? R.string.developer_menu_ac3_support_yes
- : R.string.developer_menu_ac3_support_no)));
- return items;
- }
-}
diff --git a/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java b/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java
index 06415c21..dec017a8 100644
--- a/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java
+++ b/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java
@@ -156,8 +156,11 @@ public class PipInputSelectorFragment extends SideFragment {
// If this input shares the same parent with the current main input, you cannot select
// it. (E.g. two HDMI CEC devices that are connected to HDMI port 1 through an A/V
// receiver.)
- TvInputInfo mainInputInfo = mPipInputManager.getPipInput(
- getMainActivity().getCurrentChannel()).getInputInfo();
+ PipInput pipInput = mPipInputManager.getPipInput(getMainActivity().getCurrentChannel());
+ if (pipInput == null) {
+ return false;
+ }
+ TvInputInfo mainInputInfo = pipInput.getInputInfo();
TvInputInfo pipInputInfo = mPipInput.getInputInfo();
return mainInputInfo == null || pipInputInfo == null
|| !TextUtils.equals(mainInputInfo.getId(), pipInputInfo.getId())
diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
index 6b5b2584..6d606014 100644
--- a/src/com/android/tv/ui/sidepanel/SettingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
@@ -16,13 +16,9 @@
package com.android.tv.ui.sidepanel;
-import android.content.res.Resources;
-import android.os.Build;
-import android.provider.Settings;
import android.view.View;
import android.widget.Toast;
-import com.android.tv.Features;
import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.TvApplication;
@@ -155,19 +151,6 @@ public class SettingsFragment extends SideFragment {
if (LicenseUtils.hasLicenses(activity.getAssets())) {
items.add(new LicenseActionItem(activity));
}
- boolean developerOptionEnabled = Settings.Secure.getInt(getActivity().getContentResolver(),
- Settings.Global.DEVELOPMENT_SETTINGS_ENABLED , 0) != 0;
- if (Features.DEVELOPER_OPTION.isEnabled(getActivity()) && developerOptionEnabled
- && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- Resources res = getActivity().getResources();
- items.add(new ActionItem(res.getString(R.string.side_panel_title_developer)) {
- @Override
- protected void onSelected() {
- getMainActivity().getOverlayManager().getSideFragmentManager().show(
- new DeveloperFragment());
- }
- });
- }
// Show version.
items.add(new SimpleItem(getString(R.string.settings_menu_version),
((TvApplication) activity.getApplicationContext()).getVersionName()));
diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
index 7ec28bb8..6bc47939 100644
--- a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
@@ -18,6 +18,7 @@ package com.android.tv.ui.sidepanel.parentalcontrols;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
+import android.util.ArrayMap;
import android.util.SparseIntArray;
import android.view.View;
import android.widget.CompoundButton;
@@ -41,6 +42,7 @@ import com.android.tv.util.TvSettings.ContentRatingLevel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
public class RatingsFragment extends SideFragment {
private static final SparseIntArray sLevelResourceIdMap;
@@ -73,6 +75,9 @@ public class RatingsFragment extends SideFragment {
}
private final List<RatingLevelItem> mRatingLevelItems = new ArrayList<>();
+ // A map from the rating system ID string to RatingItem objects.
+ private final Map<String, List<RatingItem>> mContentRatingSystemItemMap = new ArrayMap<>();
+ private ParentalControlSettings mParentalControlSettings;
public static String getDescription(MainActivity tvActivity) {
@ContentRatingLevel int currentLevel =
@@ -104,18 +109,32 @@ public class RatingsFragment extends SideFragment {
updateRatingLevels();
items.addAll(mRatingLevelItems);
+ mContentRatingSystemItemMap.clear();
+
List<ContentRatingSystem> contentRatingSystems =
getMainActivity().getContentRatingsManager().getContentRatingSystems();
Collections.sort(contentRatingSystems, ContentRatingSystem.DISPLAY_NAME_COMPARATOR);
for (ContentRatingSystem s : contentRatingSystems) {
- if (getMainActivity().getParentalControlSettings().isContentRatingSystemEnabled(s)) {
+ if (mParentalControlSettings.isContentRatingSystemEnabled(s)) {
+ List<RatingItem> ratingItems = new ArrayList<>();
+ boolean hasSubRating = false;
items.add(new DividerItem(s.getDisplayName()));
for (Rating rating : s.getRatings()) {
- RatingItem item = rating.getSubRatings().size() == 0 ?
+ RatingItem item = rating.getSubRatings().isEmpty() ?
new RatingItem(s, rating) :
new RatingWithSubItem(s, rating);
items.add(item);
+ if (rating.getSubRatings().isEmpty()) {
+ ratingItems.add(item);
+ } else {
+ hasSubRating = true;
+ }
+ }
+ // Only include rating systems that don't contain any sub ratings in the map for
+ // simplicity.
+ if (!hasSubRating) {
+ mContentRatingSystemItemMap.put(s.getId(), ratingItems);
}
}
}
@@ -131,7 +150,8 @@ public class RatingsFragment extends SideFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- getMainActivity().getParentalControlSettings().loadRatings();
+ mParentalControlSettings = getMainActivity().getParentalControlSettings();
+ mParentalControlSettings.loadRatings();
}
@Override
@@ -146,13 +166,27 @@ public class RatingsFragment extends SideFragment {
}
private void updateRatingLevels() {
- @ContentRatingLevel int ratingLevel =
- getMainActivity().getParentalControlSettings().getContentRatingLevel();
+ @ContentRatingLevel int ratingLevel = mParentalControlSettings.getContentRatingLevel();
for (RatingLevelItem ratingLevelItem : mRatingLevelItems) {
ratingLevelItem.setChecked(ratingLevel == ratingLevelItem.mRatingLevel);
}
}
+ private void updateDependentRatingItems(ContentRatingSystem.Order order,
+ int selectedRatingOrderIndex, String contentRatingSystemId, boolean isChecked) {
+ List<RatingItem> ratingItems = mContentRatingSystemItemMap.get(contentRatingSystemId);
+ if (ratingItems != null) {
+ for (RatingItem item : ratingItems) {
+ int ratingOrderIndex = item.getRatingOrderIndex(order);
+ if (ratingOrderIndex != -1
+ && ((ratingOrderIndex > selectedRatingOrderIndex && isChecked)
+ || (ratingOrderIndex < selectedRatingOrderIndex && !isChecked))) {
+ item.setRatingBlocked(isChecked);
+ }
+ }
+ }
+ }
+
private class RatingLevelItem extends RadioButtonItem {
private final int mRatingLevel;
@@ -166,7 +200,7 @@ public class RatingsFragment extends SideFragment {
@Override
protected void onSelected() {
super.onSelected();
- getMainActivity().getParentalControlSettings().setContentRatingLevel(
+ mParentalControlSettings.setContentRatingLevel(
getMainActivity().getContentRatingsManager(), mRatingLevel);
notifyItemsChanged(mRatingLevelItems.size());
}
@@ -177,12 +211,21 @@ public class RatingsFragment extends SideFragment {
protected final Rating mRating;
private final Drawable mIcon;
private CompoundButton mCompoundButton;
+ private final List<ContentRatingSystem.Order> mOrders = new ArrayList<>();
+ private final List<Integer> mOrderIndexes = new ArrayList<>();
private RatingItem(ContentRatingSystem contentRatingSystem, Rating rating) {
super(rating.getTitle(), rating.getDescription());
mContentRatingSystem = contentRatingSystem;
mRating = rating;
mIcon = rating.getIcon();
+ for (ContentRatingSystem.Order order : mContentRatingSystem.getOrders()) {
+ int orderIndex = order.getRatingIndex(mRating);
+ if (orderIndex != -1) {
+ mOrders.add(order);
+ mOrderIndexes.add(orderIndex);
+ }
+ }
}
@Override
@@ -211,17 +254,21 @@ public class RatingsFragment extends SideFragment {
protected void onUpdate() {
super.onUpdate();
mCompoundButton.setButtonDrawable(getButtonDrawable());
- setChecked(getMainActivity().getParentalControlSettings().isRatingBlocked(
- mContentRatingSystem, mRating));
+ setChecked(mParentalControlSettings.isRatingBlocked(mContentRatingSystem, mRating));
}
@Override
protected void onSelected() {
super.onSelected();
- if (getMainActivity().getParentalControlSettings()
- .setRatingBlocked(mContentRatingSystem, mRating, isChecked())) {
+ if (mParentalControlSettings.setRatingBlocked(
+ mContentRatingSystem, mRating, isChecked())) {
updateRatingLevels();
}
+ // Automatically check/uncheck dependent ratings.
+ for (int i = 0; i < mOrders.size(); i++) {
+ updateDependentRatingItems(mOrders.get(i), mOrderIndexes.get(i),
+ mContentRatingSystem.getId(), isChecked());
+ }
}
@Override
@@ -232,6 +279,19 @@ public class RatingsFragment extends SideFragment {
protected int getButtonDrawable() {
return R.drawable.btn_lock_material_anim;
}
+
+ private int getRatingOrderIndex(ContentRatingSystem.Order order) {
+ int orderIndex = mOrders.indexOf(order);
+ return orderIndex == -1 ? -1 : mOrderIndexes.get(orderIndex);
+ }
+
+ private void setRatingBlocked(boolean isChecked) {
+ if (isChecked() == isChecked) {
+ return;
+ }
+ mParentalControlSettings.setRatingBlocked(mContentRatingSystem, mRating, isChecked);
+ notifyUpdated();
+ }
}
private class RatingWithSubItem extends RatingItem {
@@ -247,7 +307,7 @@ public class RatingsFragment extends SideFragment {
@Override
protected int getButtonDrawable() {
- int blockedStatus = getMainActivity().getParentalControlSettings().getBlockedStatus(
+ int blockedStatus = mParentalControlSettings.getBlockedStatus(
mContentRatingSystem, mRating);
if (blockedStatus == ParentalControlSettings.RATING_BLOCKED) {
return R.drawable.btn_lock_material;
diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java
index 9f440533..7ac293fc 100644
--- a/src/com/android/tv/util/AsyncDbTask.java
+++ b/src/com/android/tv/util/AsyncDbTask.java
@@ -22,10 +22,12 @@ import android.media.tv.TvContract;
import android.net.Uri;
import android.os.AsyncTask;
import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import android.util.Log;
import android.util.Range;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
@@ -40,6 +42,10 @@ import java.util.concurrent.RejectedExecutionException;
*
* <p>Instances of this class should only be executed this using {@link
* #executeOnDbThread(Object[])}.
+ *
+ * @param <Params> the type of the parameters sent to the task upon execution.
+ * @param <Progress> the type of the progress units published during the background computation.
+ * @param <Result> the type of the result of the background computation.
*/
public abstract class AsyncDbTask<Params, Progress, Result>
extends AsyncTask<Params, Progress, Result> {
@@ -79,7 +85,7 @@ public abstract class AsyncDbTask<Params, Progress, Result>
* <p> {@link #doInBackground(Void...)} executes the query on call {@link #onQuery(Cursor)}
* which is implemented by subclasses.
*
- * @param <Result> The type of result returned by {@link #onQuery(Cursor)}
+ * @param <Result> the type of result returned by {@link #onQuery(Cursor)}
*/
public abstract static class AsyncQueryTask<Result> extends AsyncDbTask<Void, Void, Result> {
private final ContentResolver mContentResolver;
@@ -103,9 +109,10 @@ public abstract class AsyncDbTask<Params, Progress, Result>
@Override
protected final Result doInBackground(Void... params) {
if (!THREAD_FACTORY.namedWithPrefix(Thread.currentThread())) {
- IllegalStateException e = new IllegalStateException(
- this + " should only be executed using executeOnDbThread, " +
- "but it was called on thread " + Thread.currentThread());
+ IllegalStateException e = new IllegalStateException(this
+ + " should only be executed using executeOnDbThread, "
+ + "but it was called on thread "
+ + Thread.currentThread());
Log.w(TAG, e);
if (DEBUG) {
throw e;
@@ -137,8 +144,8 @@ public abstract class AsyncDbTask<Params, Progress, Result>
}
return null;
}
- } catch (SecurityException e) {
- Log.d(TAG, "Security exception during query", e);
+ } catch (Exception e) {
+ SoftPreconditions.warn(TAG, null, "Error querying " + this, e);
return null;
}
}
@@ -161,8 +168,10 @@ public abstract class AsyncDbTask<Params, Progress, Result>
* Returns the result of a query as an {@link List} of {@code T}.
*
* <p>Subclasses must implement {@link #fromCursor(Cursor)}.
+ *
+ * @param <T> the type of result returned in a list by {@link #onQuery(Cursor)}
*/
- public static abstract class AsyncQueryListTask<T> extends AsyncQueryTask<List<T>> {
+ public abstract static class AsyncQueryListTask<T> extends AsyncQueryTask<List<T>> {
public AsyncQueryListTask(ContentResolver contentResolver, Uri uri, String[] projection,
String selection, String[] selectionArgs, String orderBy) {
@@ -201,9 +210,56 @@ public abstract class AsyncDbTask<Params, Progress, Result>
}
/**
+ * Returns the result of a query as a single instance of {@code T}.
+ *
+ * <p>Subclasses must implement {@link #fromCursor(Cursor)}.
+ */
+ public abstract static class AsyncQueryItemTask<T> extends AsyncQueryTask<T> {
+
+ public AsyncQueryItemTask(ContentResolver contentResolver, Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String orderBy) {
+ super(contentResolver, uri, projection, selection, selectionArgs, orderBy);
+ }
+
+ @Override
+ protected final T onQuery(Cursor c) {
+ if (c.moveToNext()) {
+ if (isCancelled()) {
+ // This is guaranteed to never call onPostExecute because the task is canceled.
+ return null;
+ }
+ T result = fromCursor(c);
+ if (c.moveToNext()) {
+ Log.w(TAG, "More than one result for found for " + this);
+ }
+ return result;
+ } else {
+ if (DEBUG) {
+ Log.v(TAG, "No result for found for " + this);
+ }
+ return null;
+ }
+
+ }
+
+ /**
+ * Return a single instance of {@code T} from the cursor.
+ *
+ * <p><b>NOTE</b> Do not move the cursor or close it, that is handled by {@link
+ * #onQuery(Cursor)}.
+ *
+ * <p><b>Note</b> This is executed on the DB thread by {@link #onQuery(Cursor)}
+ *
+ * @param c The cursor with the values to create T from.
+ */
+ @WorkerThread
+ protected abstract T fromCursor(Cursor c);
+ }
+
+ /**
* Gets an {@link List} of {@link Channel}s from {@link TvContract.Channels#CONTENT_URI}.
*/
- public static abstract class AsyncChannelQueryTask extends AsyncQueryListTask<Channel> {
+ public abstract static class AsyncChannelQueryTask extends AsyncQueryListTask<Channel> {
public AsyncChannelQueryTask(ContentResolver contentResolver) {
super(contentResolver, TvContract.Channels.CONTENT_URI, Channel.PROJECTION,
@@ -227,16 +283,19 @@ public abstract class AsyncDbTask<Params, Progress, Result>
/**
* Gets an {@link List} of {@link Program}s for a given channel and period {@link
- * TvContract#buildProgramsUriForChannel(long, long, long)}.
+ * TvContract#buildProgramsUriForChannel(long, long, long)}. If the {@code period} is
+ * {@code null}, then all the programs is queried.
*/
public static class LoadProgramsForChannelTask extends AsyncQueryListTask<Program> {
protected final Range<Long> mPeriod;
protected final long mChannelId;
public LoadProgramsForChannelTask(ContentResolver contentResolver, long channelId,
- Range<Long> period) {
- super(contentResolver, TvContract
- .buildProgramsUriForChannel(channelId, period.getLower(), period.getUpper()),
+ @Nullable Range<Long> period) {
+ super(contentResolver, period == null
+ ? TvContract.buildProgramsUriForChannel(channelId)
+ : TvContract.buildProgramsUriForChannel(channelId, period.getLower(),
+ period.getUpper()),
Program.PROJECTION, null, null, null);
mPeriod = period;
mChannelId = channelId;
diff --git a/src/com/android/tv/util/ImageLoader.java b/src/com/android/tv/util/ImageLoader.java
index 59c4983b..ed0fd54d 100644
--- a/src/com/android/tv/util/ImageLoader.java
+++ b/src/com/android/tv/util/ImageLoader.java
@@ -28,18 +28,23 @@ import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.annotation.WorkerThread;
+import android.util.ArraySet;
import android.util.Log;
import com.android.tv.R;
-import com.android.tv.common.CollectionUtils;
import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
/**
* This class wraps up completing some arbitrary long running work when loading a bitmap. It
@@ -49,6 +54,39 @@ public final class ImageLoader {
private static final String TAG = "ImageLoader";
private static final boolean DEBUG = false;
+ private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
+ // We want at least 2 threads and at most 4 threads in the core pool,
+ // preferring to have 1 less than the CPU count to avoid saturating
+ // the CPU with background work
+ private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
+ private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
+ private static final int KEEP_ALIVE_SECONDS = 30;
+
+ private static final ThreadFactory sThreadFactory = new NamedThreadFactory("ImageLoader");
+
+ private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<Runnable>(
+ 128);
+
+ /**
+ * An private {@link Executor} that can be used to execute tasks in parallel.
+ *
+ * <p>{@code IMAGE_THREAD_POOL_EXECUTOR} setting are copied from {@link AsyncTask}
+ * Since we do a lot of concurrent image loading we can exhaust a thread pool.
+ * ImageLoader catches the error, and just leaves the image blank.
+ * However other tasks will fail and crash the application.
+ *
+ * <p>Using a separate thread pool prevents image loading from causing other tasks to fail.
+ */
+ private static final Executor IMAGE_THREAD_POOL_EXECUTOR;
+
+ static {
+ ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
+ MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, sPoolWorkQueue,
+ sThreadFactory);
+ threadPoolExecutor.allowCoreThreadTimeOut(true);
+ IMAGE_THREAD_POOL_EXECUTOR = threadPoolExecutor;
+ }
+
private static Handler sMainHandler;
/**
@@ -148,7 +186,7 @@ public final class ImageLoader {
Log.d(TAG, "loadBitmap() " + uriString);
}
return doLoadBitmap(context, uriString, maxWidth, maxHeight, callback,
- AsyncTask.THREAD_POOL_EXECUTOR);
+ IMAGE_THREAD_POOL_EXECUTOR);
}
private static boolean doLoadBitmap(Context context, String uriString,
@@ -179,7 +217,7 @@ public final class ImageLoader {
if (DEBUG) {
Log.d(TAG, "loadBitmap() " + loadBitmapTask);
}
- return doLoadBitmap(callback, AsyncTask.THREAD_POOL_EXECUTOR, loadBitmapTask);
+ return doLoadBitmap(callback, IMAGE_THREAD_POOL_EXECUTOR, loadBitmapTask);
}
/**
@@ -226,7 +264,7 @@ public final class ImageLoader {
protected final Context mAppContext;
protected final int mMaxWidth;
protected final int mMaxHeight;
- private final Set<ImageLoaderCallback> mCallbacks = CollectionUtils.createSmallSet();
+ private final Set<ImageLoaderCallback> mCallbacks = new ArraySet<>();
private final ImageCache mImageCache;
private final String mKey;
diff --git a/src/com/android/tv/util/MultiLongSparseArray.java b/src/com/android/tv/util/MultiLongSparseArray.java
index 7ed72d61..1d5fa80b 100644
--- a/src/com/android/tv/util/MultiLongSparseArray.java
+++ b/src/com/android/tv/util/MultiLongSparseArray.java
@@ -17,10 +17,9 @@
package com.android.tv.util;
import android.support.annotation.VisibleForTesting;
+import android.util.ArraySet;
import android.util.LongSparseArray;
-import com.android.tv.common.CollectionUtils;
-
import java.util.Collections;
import java.util.Set;
@@ -105,7 +104,7 @@ public class MultiLongSparseArray<T> {
private Set<T> getEmptySet() {
if (mEmptyIndex < 0) {
- return CollectionUtils.createSmallSet();
+ return new ArraySet<>();
}
Set<T> emptySet = mEmptySets[mEmptyIndex];
mEmptySets[mEmptyIndex--] = null;
diff --git a/src/com/android/tv/util/NetworkUtils.java b/src/com/android/tv/util/NetworkUtils.java
new file mode 100644
index 00000000..ed3ce383
--- /dev/null
+++ b/src/com/android/tv/util/NetworkUtils.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.util;
+
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/**
+ * A utility class to check the connectivity.
+ */
+@WorkerThread
+public class NetworkUtils {
+ private static final String GENERATE_204 = "http://clients3.google.com/generate_204";
+
+ /**
+ * Checks if the internet connection is available.
+ */
+ public static boolean isNetworkAvailable(@Nullable ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return false;
+ }
+ NetworkInfo info = connectivityManager.getActiveNetworkInfo();
+ if (info == null || !info.isConnected()) {
+ return false;
+ }
+ HttpURLConnection connection = null;
+ try {
+ connection = (HttpURLConnection) new URL(GENERATE_204).openConnection();
+ connection.setInstanceFollowRedirects(false);
+ connection.setDefaultUseCaches(false);
+ connection.setUseCaches(false);
+ if (connection.getResponseCode() == HttpURLConnection.HTTP_NO_CONTENT) {
+ return true;
+ }
+ } catch (IOException e) {
+ // Does nothing.
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ return false;
+ }
+
+ private NetworkUtils() { }
+}
diff --git a/src/com/android/tv/util/OnboardingUtils.java b/src/com/android/tv/util/OnboardingUtils.java
index 582a0c9f..3dcc324d 100644
--- a/src/com/android/tv/util/OnboardingUtils.java
+++ b/src/com/android/tv/util/OnboardingUtils.java
@@ -37,7 +37,7 @@ public final class OnboardingUtils {
private static final int ONBOARDING_VERSION = 1;
private static final String MERCHANT_COLLECTION_URL_STRING =
- "https://play.google.com/store/apps/collection/promotion_3001bf9_ATV_livechannels";
+ "TODO: put a market link to show TV input apps";
/**
* Intent to show merchant collection in play store.
*/
@@ -101,7 +101,7 @@ public final class OnboardingUtils {
ContentResolver resolver = context.getContentResolver();
try (Cursor c = resolver.query(Channels.CONTENT_URI, new String[] {Channels._ID}, null,
null, null)) {
- return c.getCount() != 0;
+ return c != null && c.getCount() != 0;
}
}
diff --git a/src/com/android/tv/util/PipInputManager.java b/src/com/android/tv/util/PipInputManager.java
index dddc82b6..03bdc681 100644
--- a/src/com/android/tv/util/PipInputManager.java
+++ b/src/com/android/tv/util/PipInputManager.java
@@ -20,11 +20,11 @@ import android.content.Context;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputManager.TvInputCallback;
+import android.util.ArraySet;
import android.util.Log;
import com.android.tv.ChannelTuner;
import com.android.tv.R;
-import com.android.tv.common.CollectionUtils;
import com.android.tv.data.Channel;
import java.util.ArrayList;
@@ -37,6 +37,7 @@ import java.util.Set;
/**
* A class that manages inputs for PIP. All tuner inputs are represented to one tuner input for PIP.
+ * Hidden inputs should not be visible to the users.
*/
public class PipInputManager {
private static final String TAG = "PipInputManager";
@@ -50,7 +51,7 @@ public class PipInputManager {
private final ChannelTuner mChannelTuner;
private boolean mStarted;
private final Map<String, PipInput> mPipInputMap = new HashMap<>(); // inputId -> PipInput
- private final Set<Listener> mListeners = CollectionUtils.createSmallSet();
+ private final Set<Listener> mListeners = new ArraySet<>();
private final TvInputCallback mTvInputCallback = new TvInputCallback() {
@Override
@@ -182,40 +183,53 @@ public class PipInputManager {
/**
* Gets the size of inputs for PIP.
*
- * @param availableOnly If true, it counts only available PIP inputs. Please see {@link
+ * <p>The hidden inputs are not counted.
+ *
+ * @param availableOnly If {@code true}, it counts only available PIP inputs. Please see {@link
* PipInput#isAvailable()} for the details of availability.
*/
public int getPipInputSize(boolean availableOnly) {
- if (availableOnly) {
- int count = 0;
- for (PipInput pipInput : mPipInputMap.values()) {
- if (pipInput.isAvailable()) {
- ++count;
+ int count = 0;
+ for (PipInput pipInput : mPipInputMap.values()) {
+ if (!pipInput.isHidden() && (!availableOnly || pipInput.mAvailable)) {
+ ++count;
+ }
+ if (pipInput.isPassthrough()) {
+ TvInputInfo info = pipInput.getInputInfo();
+ // Do not count HDMI ports if a CEC device is directly connected to the port.
+ if (info.getParentId() != null && !info.isConnectedToHdmiSwitch()) {
+ --count;
}
}
- return count;
- } else {
- return mPipInputMap.size();
}
+ return count;
}
/**
- * Gets the list of inputs for PIP.
+ * Gets the list of inputs for PIP..
+ *
+ * <p>The hidden inputs are excluded.
*
* @param availableOnly If true, it returns only available PIP inputs. Please see {@link
* PipInput#isAvailable()} for the details of availability.
*/
public List<PipInput> getPipInputList(boolean availableOnly) {
- List<PipInput> pipInputs;
- if (availableOnly) {
- pipInputs = new ArrayList<>();
- for (PipInput pipInput : mPipInputMap.values()) {
- if (pipInput.mAvailable) {
- pipInputs.add(pipInput);
+ List<PipInput> pipInputs = new ArrayList<>();
+ List<PipInput> removeInputs = new ArrayList<>();
+ for (PipInput pipInput : mPipInputMap.values()) {
+ if (!pipInput.isHidden() && (!availableOnly || pipInput.mAvailable)) {
+ pipInputs.add(pipInput);
+ }
+ if (pipInput.isPassthrough()) {
+ TvInputInfo info = pipInput.getInputInfo();
+ // Do not show HDMI ports if a CEC device is directly connected to the port.
+ if (info.getParentId() != null && !info.isConnectedToHdmiSwitch()) {
+ removeInputs.add(mPipInputMap.get(info.getParentId()));
}
}
- } else {
- pipInputs = new ArrayList<>(mPipInputMap.values());
+ }
+ if (!removeInputs.isEmpty()) {
+ pipInputs.removeAll(removeInputs);
}
Collections.sort(pipInputs, new Comparator<PipInput>() {
@Override
@@ -325,9 +339,9 @@ public class PipInputManager {
}
/**
- * Returns true, if the input is available for PIP. If a channel of an input is already
- * played or an input is not connected state or there is no browsable channel, the input
- * is unavailable.
+ * Returns {@code true}, if the input is available for PIP. If a channel of an input is
+ * already played or an input is not connected state or there is no browsable channel, the
+ * input is unavailable.
*/
public boolean isAvailable() {
return mAvailable;
@@ -407,5 +421,10 @@ public class PipInputManager {
}
}
}
+
+ private boolean isHidden() {
+ // mInputInfo is null for the tuner input and it's always visible.
+ return mInputInfo != null && mInputInfo.isHidden(mContext);
+ }
}
}
diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java
index 2a006f9e..5e65715e 100644
--- a/src/com/android/tv/util/RecurringRunner.java
+++ b/src/com/android/tv/util/RecurringRunner.java
@@ -24,6 +24,7 @@ import android.support.annotation.WorkerThread;
import android.util.Log;
import com.android.tv.common.SharedPreferencesUtils;
+import com.android.tv.common.SoftPreconditions;
import java.util.Date;
diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java
index d337139b..6d24d5bd 100644
--- a/src/com/android/tv/util/SetupUtils.java
+++ b/src/com/android/tv/util/SetupUtils.java
@@ -27,11 +27,13 @@ import android.os.Build;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
+import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.Log;
import com.android.tv.ApplicationSingletons;
import com.android.tv.TvApplication;
-import com.android.tv.common.CollectionUtils;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
@@ -67,13 +69,13 @@ public class SetupUtils {
private SetupUtils(TvApplication tvApplication) {
mTvApplication = tvApplication;
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(tvApplication);
- mSetUpInputs = CollectionUtils.createSmallSet();
+ mSetUpInputs = new ArraySet<>();
mSetUpInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_SET_UP_INPUTS,
Collections.<String>emptySet()));
- mKnownInputs = CollectionUtils.createSmallSet();
+ mKnownInputs = new ArraySet<>();
mKnownInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_KNOWN_INPUTS,
Collections.<String>emptySet()));
- mRecognizedInputs = CollectionUtils.createSmallSet();
+ mRecognizedInputs = new ArraySet<>();
mRecognizedInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS,
mKnownInputs));
mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true);
@@ -262,29 +264,26 @@ public class SetupUtils {
* @param context The Context used for granting permission.
*/
public static void grantEpgPermissionToSetUpPackages(Context context) {
- // TvProvider allows granting of Uri permissions starting from MNC.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- SharedPreferences sharedPreferences =
- PreferenceManager.getDefaultSharedPreferences(context);
- Set<String> setUpInputs = new HashSet<>(sharedPreferences.getStringSet(
- PREF_KEY_SET_UP_INPUTS, new HashSet<String>()));
- Set<String> setUpPackages = new HashSet<>();
- for (String input : setUpInputs) {
- 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;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ // Can't grant permission.
+ return;
+ }
+
+ // Find all already-verified packages.
+ Set<String> setUpPackages = new HashSet<>();
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
+ for (String input : sp.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.EMPTY_SET)) {
+ if (!TextUtils.isEmpty(input)) {
+ ComponentName componentName = ComponentName.unflattenFromString(input);
+ if (componentName != null) {
+ setUpPackages.add(componentName.getPackageName());
}
- setUpPackages.add(componentName.getPackageName());
- }
- for (String packageName : setUpPackages) {
- grantEpgPermission(context, packageName);
}
}
+
+ for (String packageName : setUpPackages) {
+ grantEpgPermission(context, packageName);
+ }
}
/**
@@ -346,7 +345,7 @@ public class SetupUtils {
}
mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs)
.putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs)
- .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs).apply();
+ .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs).apply();
}
}
@@ -360,7 +359,7 @@ public class SetupUtils {
if (!mRecognizedInputs.contains(inputId)) {
Log.i(TAG, "An unrecognized input's setup has been done. inputId=" + inputId);
mRecognizedInputs.add(inputId);
- mSharedPreferences.edit().putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs)
+ mSharedPreferences.edit().putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs)
.apply();
}
if (!mKnownInputs.contains(inputId)) {
diff --git a/src/com/android/tv/util/SoftPreconditions.java b/src/com/android/tv/util/SoftPreconditions.java
deleted file mode 100644
index 3643fca4..00000000
--- a/src/com/android/tv/util/SoftPreconditions.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.tv.util;
-
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.tv.common.BuildConfig;
-import com.android.tv.common.feature.Feature;
-
-/**
- * Simple static methods to be called at the start of your own methods to verify
- * correct arguments and state.
- *
- * <p>{@code checkXXX} methods throw exceptions when {@link BuildConfig#ENG} is true, and
- * logs a warning when it is false.
- *
- * <p>This is based on com.android.internal.util.Preconditions.
- */
-public final class SoftPreconditions {
- private static final String TAG = "SoftPreconditions";
-
- /**
- * Throws or logs if an expression involving the parameter of the calling
- * method is not true.
- *
- * @param expression a boolean expression
- * @param tag Used to identify the source of a log message. It usually
- * identifies the class or activity where the log call occurs.
- * @param msg The message you would like logged.
- * @throws IllegalArgumentException if {@code expression} is true
- */
- public static void checkArgument(final boolean expression, String tag, String msg) {
- if (!expression) {
- warn(tag, "Illegal argument", msg, new IllegalArgumentException(msg));
- }
- }
-
- /**
- * Throws or logs if an expression involving the parameter of the calling
- * method is not true.
- *
- * @param expression a boolean expression
- * @throws IllegalArgumentException if {@code expression} is true
- */
- public static void checkArgument(final boolean expression) {
- checkArgument(expression, null, null);
- }
-
- /**
- * Throws or logs if an and object is null.
- *
- * @param reference an object reference
- * @param tag Used to identify the source of a log message. It usually
- * identifies the class or activity where the log call occurs.
- * @param msg The message you would like logged.
- * @return true if the object is null
- * @throws NullPointerException if {@code reference} is null
- */
- public static <T> T checkNotNull(final T reference, String tag, String msg) {
- if (reference == null) {
- warn(tag, "Null Pointer", msg, new NullPointerException(msg));
- }
- return reference;
- }
-
- /**
- * Throws or logs if an and object is null.
- *
- * @param reference an object reference
- * @return true if the object is null
- * @throws NullPointerException if {@code reference} is null
- */
- public static <T> T checkNotNull(final T reference) {
- return checkNotNull(reference, null, null);
- }
-
- /**
- * Throws or logs if an expression involving the state of the calling
- * instance, but not involving any parameters to the calling method is not true.
- *
- * @param expression a boolean expression
- * @param tag Used to identify the source of a log message. It usually
- * identifies the class or activity where the log call occurs.
- * @param msg The message you would like logged.
- * @throws IllegalStateException if {@code expression} is true
- */
- public static void checkState(final boolean expression, String tag, String msg) {
- if (!expression) {
- warn(tag, "Illegal State", msg, new IllegalStateException(msg));
- }
- }
-
- /**
- * Throws or logs if an expression involving the state of the calling
- * instance, but not involving any parameters to the calling method is not true.
- *
- * @param expression a boolean expression
- * @throws IllegalStateException if {@code expression} is true
- */
- public static void checkState(final boolean expression) {
- checkState(expression, null, null);
- }
-
- /**
- * Throws 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
- * identifies the class or activity where the log call occurs.
- * @param msg The message you would like logged
- * @param e The exception to throw
- */
- public static void warn(String tag, String prefix, String msg, RuntimeException e)
- throws RuntimeException{
- if (BuildConfig.ENG) {
- throw e;
- } else {
- if (TextUtils.isEmpty(tag)) {
- tag = TAG;
- }
- String logMessage;
- if (TextUtils.isEmpty(msg)) {
- logMessage = prefix;
- } else if (TextUtils.isEmpty(prefix)) {
- logMessage = msg;
- } else {
- logMessage = prefix + ": " + msg;
- }
- Log.w(tag, logMessage, e);
- }
- }
-
- private SoftPreconditions() {
- }
-}
diff --git a/src/com/android/tv/util/SystemProperties.java b/src/com/android/tv/util/SystemProperties.java
index 1dc70fd5..235161b6 100644
--- a/src/com/android/tv/util/SystemProperties.java
+++ b/src/com/android/tv/util/SystemProperties.java
@@ -58,13 +58,6 @@ public final class SystemProperties {
public static final BooleanSystemProperty USE_TRACKER = new BooleanSystemProperty(
"tv_use_tracker", true);
- /**
- * Selects using {@link com.android.tv.dvr.DvrDataManagerInMemoryImpl}
- * instead of {@link com.android.tv.dvr.DvrDataManagerImpl}
- */
- public static final BooleanSystemProperty USE_IN_MEMORY_DVR_DB = new BooleanSystemProperty(
- "tv_use_in_memory_dvr_db", false); // STOPSHIP(DVR)
-
static {
updateSystemProperties();
}
diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java
index 250ca430..b4149637 100644
--- a/src/com/android/tv/util/TvInputManagerHelper.java
+++ b/src/com/android/tv/util/TvInputManagerHelper.java
@@ -26,6 +26,7 @@ import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java
index 44d601c5..a763fe58 100644
--- a/src/com/android/tv/util/Utils.java
+++ b/src/com/android/tv/util/Utils.java
@@ -24,10 +24,10 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
+import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.database.Cursor;
-import android.media.tv.TvContentRating;
import android.media.tv.TvContract;
import android.media.tv.TvContract.Channels;
import android.media.tv.TvInputInfo;
@@ -42,18 +42,16 @@ import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.View;
+import android.widget.Toast;
-import com.android.tv.Features;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.data.StreamInfo;
-import com.android.usbtuner.tvinput.UsbTunerTvInputService;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
@@ -336,6 +334,19 @@ public class Utils {
return "";
}
+ public static String getAspectRatioString(float videoDisplayAspectRatio) {
+ if (videoDisplayAspectRatio <= 0) {
+ return "";
+ }
+
+ for (AspectRatio ratio : AspectRatio.values()) {
+ if (Math.abs((float) ratio.width / ratio.height - videoDisplayAspectRatio) < 0.05f) {
+ return ratio.toString();
+ }
+ }
+ return "";
+ }
+
public static int getVideoDefinitionLevelFromSize(int width, int height) {
if (width >= VIDEO_ULTRA_HD_WIDTH && height >= VIDEO_ULTRA_HD_HEIGHT) {
return StreamInfo.VIDEO_DEFINITION_LEVEL_ULTRA_HD;
@@ -609,67 +620,46 @@ public class Utils {
}
/**
- * Returns input ID of {@link UsbTunerTvInputService}.
+ * Returns a localized version of the text resource specified by resourceId.
*/
- @Nullable
- public static String getUsbTunerInputId(Context context) {
- if (!Features.USB_TUNER.isEnabled(context)) {
- return null;
+ public static CharSequence getTextForLocale(Context context, Locale locale, int resourceId) {
+ if (locale.equals(context.getResources().getConfiguration().locale)) {
+ return context.getText(resourceId);
}
- return TvContract.buildInputId(new ComponentName(context.getPackageName(),
- UsbTunerTvInputService.class.getName()));
+ Configuration config = new Configuration(context.getResources().getConfiguration());
+ config.setLocale(locale);
+ return context.createConfigurationContext(config).getText(resourceId);
}
/**
- * Returns {@link TvInputInfo} object of {@link UsbTunerTvInputService}.
+ * Returns the internal TV inputs.
*/
- @Nullable
- public static TvInputInfo getUsbTunerInputInfo(Context context) {
- if (!Features.USB_TUNER.isEnabled(context)) {
- return null;
+ public static List<TvInputInfo> getInternalTvInputs(Context context, boolean tunerInputOnly) {
+ List<TvInputInfo> inputs = new ArrayList<>();
+ String contextPackageName = context.getPackageName();
+ for (TvInputInfo input : TvApplication.getSingletons(context).getTvInputManagerHelper()
+ .getTvInputInfos(true, tunerInputOnly)) {
+ if (contextPackageName.equals(ComponentName.unflattenFromString(input.getId())
+ .getPackageName())) {
+ inputs.add(input);
+ }
}
- TvInputManagerHelper helper = TvApplication.getSingletons(context)
- .getTvInputManagerHelper();
- return helper.getTvInputInfo(getUsbTunerInputId(context));
+ return inputs;
}
- private static final class SyncRunnable implements Runnable {
- private final Runnable mTarget;
- private boolean mComplete;
-
- public SyncRunnable(Runnable target) {
- mTarget = target;
- }
-
- @Override
- public void run() {
- try {
- mTarget.run();
- } finally {
- synchronized (this) {
- mComplete = true;
- notifyAll();
- }
- }
- }
+ /**
+ * Checks whether the input is internal or not.
+ */
+ public static boolean isInternalTvInput(Context context, String inputId) {
+ return context.getPackageName().equals(ComponentName.unflattenFromString(inputId)
+ .getPackageName());
+ }
- public void waitForComplete() {
- boolean interrupted = false;
- synchronized (this) {
- try {
- while (!mComplete) {
- try {
- wait();
- } catch (InterruptedException e) {
- interrupted = true;
- }
- }
- } finally {
- if (interrupted) {
- Thread.currentThread().interrupt();
- }
- }
- }
- }
+ /**
+ * Shows a toast message to notice that the current feature is a developer feature.
+ */
+ public static void showToastMessageForDeveloperFeature(Context context) {
+ Toast.makeText(context, "This feature is for developer preview.", Toast.LENGTH_SHORT)
+ .show();
}
}
diff --git a/src/com/android/usbtuner/UsbInputController.java b/src/com/android/usbtuner/UsbInputController.java
index 6d6fccc2..f0982eb5 100644
--- a/src/com/android/usbtuner/UsbInputController.java
+++ b/src/com/android/usbtuner/UsbInputController.java
@@ -23,10 +23,14 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
+import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvInputService;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.support.v4.os.BuildCompat;
import android.util.Log;
import com.android.tv.Features;
@@ -44,7 +48,7 @@ import java.util.Map;
* 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 boolean DEBUG = true;
private static final String TAG = "UsbInputController";
private static final TunerDevice[] TUNER_DEVICES = {
@@ -58,7 +62,7 @@ public class UsbInputController extends BroadcastReceiver {
private static final long DVB_DRIVER_CHECK_DELAY_MS = 300;
private DvbDeviceAccessor mDvbDeviceAccessor;
- private Handler mHandler = new Handler(Looper.getMainLooper()) {
+ private final Handler mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
@@ -155,6 +159,10 @@ public class UsbInputController extends BroadcastReceiver {
PackageManager pm = context.getPackageManager();
ComponentName USBTUNER = new ComponentName(context, UsbTunerTvInputService.class);
+ // Don't kill app by enabling/disabling TvActivity. If LC is killed by enabling/disabling
+ // TvActivity, the following pm.setComponentEnabledSetting doesn't work.
+ ((TvApplication) context.getApplicationContext()).handleInputCountChanged(
+ true, enabled, true);
// 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.
@@ -165,10 +173,17 @@ public class UsbInputController extends BroadcastReceiver {
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);
}
+ if (enabled && BuildCompat.isAtLeastN()) {
+ TvInputInfo info = mDvbDeviceAccessor.buildTvInputInfo(context);
+ if (info != null) {
+ Log.i(TAG, "TvInputInfo updated: " + info.toString());
+ ((TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE))
+ .updateTvInputInfo(info);
+ }
+ }
}
}