aboutsummaryrefslogtreecommitdiff
path: root/src/com/android
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android')
-rw-r--r--src/com/android/tv/ChannelChanger.java (renamed from src/com/android/tv/util/Filter.java)13
-rw-r--r--src/com/android/tv/ChannelTuner.java8
-rw-r--r--src/com/android/tv/InputSessionManager.java165
-rw-r--r--src/com/android/tv/MainActivity.java521
-rw-r--r--src/com/android/tv/MediaSessionWrapper.java53
-rw-r--r--src/com/android/tv/SetupPassthroughActivity.java52
-rw-r--r--src/com/android/tv/TimeShiftManager.java55
-rw-r--r--src/com/android/tv/TvApplication.java116
-rw-r--r--src/com/android/tv/TvSingletons.java26
-rw-r--r--src/com/android/tv/analytics/SendChannelStatusRunnable.java17
-rw-r--r--src/com/android/tv/app/LiveTvApplication.java109
-rw-r--r--src/com/android/tv/app/LiveTvApplicationComponent.java26
-rw-r--r--src/com/android/tv/app/LiveTvModule.java33
-rw-r--r--src/com/android/tv/audio/AudioManagerHelper.java (renamed from src/com/android/tv/AudioManagerHelper.java)98
-rw-r--r--src/com/android/tv/audiotvservice/AudioOnlyTvService.java102
-rw-r--r--src/com/android/tv/audiotvservice/AudioOnlyTvServiceUtil.java68
-rw-r--r--src/com/android/tv/audiotvservice/README.md18
-rw-r--r--src/com/android/tv/data/BaseProgram.java13
-rw-r--r--src/com/android/tv/data/ChannelDataManager.java30
-rw-r--r--src/com/android/tv/data/ChannelImpl.java30
-rw-r--r--src/com/android/tv/data/PreviewDataManager.java33
-rw-r--r--src/com/android/tv/data/PreviewProgramContent.java2
-rw-r--r--src/com/android/tv/data/Program.java84
-rw-r--r--src/com/android/tv/data/ProgramDataManager.java220
-rw-r--r--src/com/android/tv/data/WatchedHistoryManager.java35
-rw-r--r--src/com/android/tv/data/api/Channel.java4
-rw-r--r--src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java86
-rw-r--r--src/com/android/tv/data/epg/EpgFetchHelper.java54
-rw-r--r--src/com/android/tv/data/epg/EpgFetcherImpl.java154
-rw-r--r--src/com/android/tv/data/epg/EpgInputWhiteList.java11
-rw-r--r--src/com/android/tv/data/epg/EpgReader.java10
-rw-r--r--src/com/android/tv/dialog/PinDialogFragment.java428
-rw-r--r--src/com/android/tv/dialog/picker/PinPicker.java131
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerImpl.java441
-rw-r--r--src/com/android/tv/dvr/DvrManager.java64
-rw-r--r--src/com/android/tv/dvr/DvrScheduleManager.java8
-rw-r--r--src/com/android/tv/dvr/DvrStorageStatusManager.java8
-rw-r--r--src/com/android/tv/dvr/DvrTvView.java158
-rw-r--r--src/com/android/tv/dvr/data/RecordedProgram.java911
-rw-r--r--src/com/android/tv/dvr/data/ScheduledRecording.java55
-rw-r--r--src/com/android/tv/dvr/data/SeriesRecording.java32
-rw-r--r--src/com/android/tv/dvr/provider/DvrDatabaseHelper.java3
-rw-r--r--src/com/android/tv/dvr/provider/DvrDbFuture.java (renamed from src/com/android/tv/dvr/provider/AsyncDvrDbTask.java)124
-rw-r--r--src/com/android/tv/dvr/provider/DvrDbSync.java3
-rw-r--r--src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java8
-rw-r--r--src/com/android/tv/dvr/recorder/InputTaskScheduler.java26
-rw-r--r--src/com/android/tv/dvr/recorder/RecordingTask.java64
-rw-r--r--src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java40
-rw-r--r--src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java2
-rw-r--r--src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java2
-rw-r--r--src/com/android/tv/dvr/ui/DvrConflictFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java87
-rw-r--r--src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java27
-rw-r--r--src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrScheduleFragment.java8
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java70
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java78
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java2
-rw-r--r--src/com/android/tv/dvr/ui/DvrUiHelper.java43
-rw-r--r--src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java60
-rw-r--r--src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java8
-rw-r--r--src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java69
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsContent.java94
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java8
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java2
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java6
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java213
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java144
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java15
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java22
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordingCardView.java38
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java3
-rw-r--r--src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java17
-rw-r--r--src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java14
-rw-r--r--src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java17
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java147
-rw-r--r--src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java8
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java6
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java35
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java11
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java30
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlayer.java43
-rw-r--r--src/com/android/tv/features/PartnerFeatures.java58
-rw-r--r--src/com/android/tv/features/TvFeatures.java (renamed from src/com/android/tv/TvFeatures.java)61
-rw-r--r--src/com/android/tv/guide/ProgramGuide.java109
-rw-r--r--src/com/android/tv/guide/ProgramItemView.java37
-rw-r--r--src/com/android/tv/guide/ProgramManager.java57
-rw-r--r--src/com/android/tv/guide/ProgramRow.java22
-rw-r--r--src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java64
-rw-r--r--src/com/android/tv/guide/ProgramTableAdapter.java52
-rw-r--r--src/com/android/tv/menu/ChannelsRowAdapter.java136
-rw-r--r--src/com/android/tv/menu/ChannelsRowItem.java10
-rw-r--r--src/com/android/tv/menu/Menu.java9
-rw-r--r--src/com/android/tv/menu/MenuAction.java4
-rw-r--r--src/com/android/tv/menu/OptionsRowAdapter.java17
-rw-r--r--src/com/android/tv/menu/PlayControlsRowView.java85
-rw-r--r--src/com/android/tv/menu/TvOptionsRowAdapter.java4
-rw-r--r--src/com/android/tv/modules/TvApplicationModule.java58
-rw-r--r--src/com/android/tv/modules/TvSingletonsModule.java47
-rw-r--r--src/com/android/tv/onboarding/OnboardingActivity.java25
-rw-r--r--src/com/android/tv/onboarding/SetupSourcesFragment.java12
-rw-r--r--src/com/android/tv/parental/ContentRatingSystem.java11
-rw-r--r--src/com/android/tv/parental/ParentalControlSettings.java45
-rw-r--r--src/com/android/tv/perf/EventNames.java28
-rw-r--r--src/com/android/tv/perf/PerformanceMonitor.java13
-rw-r--r--src/com/android/tv/perf/PerformanceMonitorManager.java38
-rw-r--r--src/com/android/tv/perf/PerformanceMonitorManagerFactory.java35
-rw-r--r--src/com/android/tv/perf/StartupMeasure.java42
-rw-r--r--src/com/android/tv/perf/stub/StubPerformanceMonitor.java (renamed from src/com/android/tv/perf/StubPerformanceMonitor.java)9
-rw-r--r--src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java36
-rw-r--r--src/com/android/tv/perf/stub/StubStartupMeasure.java32
-rw-r--r--src/com/android/tv/receiver/BootCompletedReceiver.java4
-rw-r--r--src/com/android/tv/receiver/PackageIntentsReceiver.java2
-rw-r--r--src/com/android/tv/recommendation/ChannelPreviewUpdater.java36
-rw-r--r--src/com/android/tv/recommendation/RecommendationDataManager.java68
-rw-r--r--src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java363
-rw-r--r--src/com/android/tv/search/DataManagerSearch.java20
-rw-r--r--src/com/android/tv/search/LocalSearchProvider.java7
-rw-r--r--src/com/android/tv/search/ProgramGuideSearchFragment.java4
-rw-r--r--src/com/android/tv/search/SearchInterface.java1
-rw-r--r--src/com/android/tv/search/TvProviderSearch.java7
-rw-r--r--src/com/android/tv/setup/SystemSetupActivity.java8
-rw-r--r--src/com/android/tv/tuner/TunerInputController.java556
-rw-r--r--src/com/android/tv/tunerinputcontroller/BuiltInTunerManager.java23
-rw-r--r--src/com/android/tv/tunerinputcontroller/HasBuiltInTunerManager.java30
-rw-r--r--src/com/android/tv/tunerinputcontroller/TunerInputController.java37
-rw-r--r--src/com/android/tv/ui/AppLayerTvView.java4
-rw-r--r--src/com/android/tv/ui/BlockScreenView.java9
-rw-r--r--src/com/android/tv/ui/ChannelBannerView.java177
-rw-r--r--src/com/android/tv/ui/DetailsActivity.java209
-rw-r--r--src/com/android/tv/ui/FullscreenDialogView.java16
-rw-r--r--src/com/android/tv/ui/InputBannerView.java7
-rw-r--r--src/com/android/tv/ui/IntroView.java8
-rw-r--r--src/com/android/tv/ui/KeypadChannelSwitchView.java11
-rw-r--r--src/com/android/tv/ui/ProgramDetailsFragment.java359
-rw-r--r--src/com/android/tv/ui/TunableTvView.java232
-rw-r--r--src/com/android/tv/ui/TvOverlayManager.java116
-rw-r--r--src/com/android/tv/ui/TvTransitionManager.java53
-rw-r--r--src/com/android/tv/ui/TvViewUiManager.java17
-rw-r--r--src/com/android/tv/ui/api/TunableTvViewPlayingApi.java (renamed from src/com/android/tv/ui/TunableTvViewPlayingApi.java)14
-rw-r--r--src/com/android/tv/ui/hideable/AutoHideScheduler.java15
-rw-r--r--src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java20
-rw-r--r--src/com/android/tv/ui/sidepanel/MultiAudioFragment.java7
-rw-r--r--src/com/android/tv/ui/sidepanel/SettingsFragment.java13
-rw-r--r--src/com/android/tv/ui/sidepanel/SideFragment.java9
-rw-r--r--src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java13
-rw-r--r--src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java5
-rw-r--r--src/com/android/tv/util/AsyncDbTask.java105
-rw-r--r--src/com/android/tv/util/RecurringRunner.java17
-rw-r--r--src/com/android/tv/util/SetupUtils.java78
-rw-r--r--src/com/android/tv/util/SqlParams.java11
-rw-r--r--src/com/android/tv/util/TvInputManagerHelper.java176
-rw-r--r--src/com/android/tv/util/TvProviderUtils.java211
-rw-r--r--src/com/android/tv/util/TvTrackInfoUtils.java202
-rw-r--r--src/com/android/tv/util/Utils.java139
-rw-r--r--src/com/android/tv/util/images/BitmapUtils.java16
-rw-r--r--src/com/android/tv/util/images/ImageLoader.java27
159 files changed, 5859 insertions, 4854 deletions
diff --git a/src/com/android/tv/util/Filter.java b/src/com/android/tv/ChannelChanger.java
index 3e24a496..55035696 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/src/com/android/tv/ChannelChanger.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package com.android.tv;
-package com.android.tv.util;
+/** Changes the channel. */
+public interface ChannelChanger {
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
- /** Returns true, if {@code input} is acceptable. */
- boolean filter(T input);
+ void channelUp();
+
+ void channelDown();
}
diff --git a/src/com/android/tv/ChannelTuner.java b/src/com/android/tv/ChannelTuner.java
index 8ab145a4..fe138980 100644
--- a/src/com/android/tv/ChannelTuner.java
+++ b/src/com/android/tv/ChannelTuner.java
@@ -97,13 +97,7 @@ public class ChannelTuner {
mStarted = true;
mChannelDataManager.addListener(mChannelDataManagerListener);
if (mChannelDataManager.isDbLoadFinished()) {
- mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- mChannelDataManagerListener.onLoadFinished();
- }
- });
+ mHandler.post(mChannelDataManagerListener::onLoadFinished);
}
}
diff --git a/src/com/android/tv/InputSessionManager.java b/src/com/android/tv/InputSessionManager.java
index 4f298ed6..ea17751b 100644
--- a/src/com/android/tv/InputSessionManager.java
+++ b/src/com/android/tv/InputSessionManager.java
@@ -20,11 +20,8 @@ import android.annotation.TargetApi;
import android.content.Context;
import android.media.tv.TvContentRating;
import android.media.tv.TvInputInfo;
-import android.media.tv.TvRecordingClient;
-import android.media.tv.TvRecordingClient.RecordingCallback;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView;
-import android.media.tv.TvView.TvInputCallback;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -36,9 +33,15 @@ import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
+import com.android.tv.common.compat.TvRecordingClientCompat;
+import com.android.tv.common.compat.TvRecordingClientCompat.RecordingCallbackCompat;
+import com.android.tv.common.compat.TvViewCompat;
+import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat;
import com.android.tv.data.api.Channel;
+import com.android.tv.dvr.DvrTvView;
import com.android.tv.ui.TunableTvView;
import com.android.tv.ui.TunableTvView.OnTuneListener;
+import com.android.tv.ui.api.TunableTvViewPlayingApi;
import com.android.tv.util.TvInputManagerHelper;
import java.util.Collections;
import java.util.List;
@@ -87,7 +90,9 @@ public class InputSessionManager {
@MainThread
@NonNull
public TvViewSession createTvViewSession(
- TvView tvView, TunableTvView tunableTvView, TvInputCallback callback) {
+ TvViewCompat tvView,
+ TunableTvViewPlayingApi tunableTvView,
+ TvInputCallbackCompat callback) {
TvViewSession session = new TvViewSession(tvView, tunableTvView, callback);
mTvViewSessions.add(session);
if (DEBUG) Log.d(TAG, "TvView session created: " + session);
@@ -107,7 +112,7 @@ public class InputSessionManager {
public RecordingSession createRecordingSession(
String inputId,
String tag,
- RecordingCallback callback,
+ RecordingCallbackCompat callback,
Handler handler,
long endTimeMs) {
RecordingSession session = new RecordingSession(inputId, tag, callback, handler, endTimeMs);
@@ -237,9 +242,10 @@ public class InputSessionManager {
*/
@MainThread
public class TvViewSession {
- private final TvView mTvView;
- private final TunableTvView mTunableTvView;
- private final TvInputCallback mCallback;
+ private final TvViewCompat mTvView;
+ private final TunableTvViewPlayingApi mTunableTvView;
+ private final TvInputCallbackCompat mCallback;
+ private final boolean mIsDvrSession;
private Channel mChannel;
private String mInputId;
private Uri mChannelUri;
@@ -248,10 +254,14 @@ public class InputSessionManager {
private boolean mTuned;
private boolean mNeedToBeRetuned;
- TvViewSession(TvView tvView, TunableTvView tunableTvView, TvInputCallback callback) {
+ TvViewSession(
+ TvViewCompat tvView,
+ TunableTvViewPlayingApi tunableTvView,
+ TvInputCallbackCompat callback) {
mTvView = tvView;
mTunableTvView = tunableTvView;
mCallback = callback;
+ mIsDvrSession = tunableTvView instanceof DvrTvView;
mTvView.setCallback(
new DelegateTvInputCallback(mCallback) {
@Override
@@ -338,9 +348,13 @@ public class InputSessionManager {
void retune() {
if (DEBUG) Log.d(TAG, "Retune requested.");
+ if (mIsDvrSession) {
+ Log.w(TAG, "DVR session should not call retune()!");
+ return;
+ }
if (mNeedToBeRetuned) {
if (DEBUG) Log.d(TAG, "Retuning: {channel=" + mChannel + "}");
- mTunableTvView.tuneTo(mChannel, mParams, mOnTuneListener);
+ ((TunableTvView) mTunableTvView).tuneTo(mChannel, mParams, mOnTuneListener);
mNeedToBeRetuned = false;
}
}
@@ -369,9 +383,13 @@ public class InputSessionManager {
void resetByRecording() {
mCallback.onVideoUnavailable(
mInputId, TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE);
+ if (mIsDvrSession) {
+ Log.w(TAG, "DVR session should not call resetByRecording()!");
+ return;
+ }
if (mTuned) {
if (DEBUG) Log.d(TAG, "Reset TvView session by recording");
- mTunableTvView.resetByRecording();
+ ((TunableTvView) mTunableTvView).resetByRecording();
reset();
}
mNeedToBeRetuned = true;
@@ -386,22 +404,22 @@ public class InputSessionManager {
public class RecordingSession {
private final String mInputId;
private Uri mChannelUri;
- private final RecordingCallback mCallback;
+ private final RecordingCallbackCompat mCallback;
private final Handler mHandler;
private volatile long mEndTimeMs;
- private TvRecordingClient mClient;
+ private TvRecordingClientCompat mClient;
private boolean mTuned;
RecordingSession(
String inputId,
String tag,
- RecordingCallback callback,
+ RecordingCallbackCompat callback,
Handler handler,
long endTimeMs) {
mInputId = inputId;
mCallback = callback;
mHandler = handler;
- mClient = new TvRecordingClient(mContext, tag, callback, handler);
+ mClient = new TvRecordingClientCompat(mContext, tag, callback, handler);
mEndTimeMs = endTimeMs;
}
@@ -409,29 +427,26 @@ public class InputSessionManager {
if (DEBUG) Log.d(TAG, "Release of recording session requested.");
runOnHandler(
mMainThreadHandler,
- new Runnable() {
- @Override
- public void run() {
- if (DEBUG) Log.d(TAG, "Releasing of recording session.");
- mTuned = false;
- mClient.release();
- mClient = null;
- for (TvViewSession session : mTvViewSessions) {
- if (DEBUG) {
- Log.d(
- TAG,
- "Finding TvView sessions for retune: {tuned="
- + session.mTuned
- + ", inputId="
- + session.mInputId
- + ", session="
- + session
- + "}");
- }
- if (!session.mTuned && Objects.equals(session.mInputId, mInputId)) {
- session.retune();
- break;
- }
+ () -> {
+ if (DEBUG) Log.d(TAG, "Releasing of recording session.");
+ mTuned = false;
+ mClient.release();
+ mClient = null;
+ for (TvViewSession session : mTvViewSessions) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Finding TvView sessions for retune: {tuned="
+ + session.mTuned
+ + ", inputId="
+ + session.mInputId
+ + ", session="
+ + session
+ + "}");
+ }
+ if (!session.mTuned && Objects.equals(session.mInputId, mInputId)) {
+ session.retune();
+ break;
}
}
});
@@ -441,42 +456,39 @@ public class InputSessionManager {
public void tune(String inputId, Uri channelUri) {
runOnHandler(
mMainThreadHandler,
- new Runnable() {
- @Override
- public void run() {
- int tunedRecordingSessionCount = getTunedRecordingSessionCount(inputId);
- TvInputInfo input = mInputManager.getTvInputInfo(inputId);
- if (input == null
- || !input.canRecord()
- || input.getTunerCount() <= tunedRecordingSessionCount) {
- runOnHandler(
- mHandler,
- new Runnable() {
- @Override
- public void run() {
- mCallback.onConnectionFailed(inputId);
- }
- });
- return;
- }
- mTuned = true;
- int tunedTuneSessionCount = getTunedTvViewSessionCount(inputId);
- if (!isTunedForTvView(channelUri)
- && tunedTuneSessionCount > 0
- && tunedRecordingSessionCount + tunedTuneSessionCount
- >= input.getTunerCount()) {
- for (TvViewSession session : mTvViewSessions) {
- if (session.mTuned
- && Objects.equals(session.mInputId, inputId)
- && !isTunedForRecording(session.mChannelUri)) {
- session.resetByRecording();
- break;
- }
+ () -> {
+ int tunedRecordingSessionCount = getTunedRecordingSessionCount(inputId);
+ TvInputInfo input = mInputManager.getTvInputInfo(inputId);
+ if (input == null
+ || !input.canRecord()
+ || input.getTunerCount() <= tunedRecordingSessionCount) {
+ runOnHandler(
+ mHandler,
+ new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onConnectionFailed(inputId);
+ }
+ });
+ return;
+ }
+ mTuned = true;
+ int tunedTuneSessionCount = getTunedTvViewSessionCount(inputId);
+ if (!isTunedForTvView(channelUri)
+ && tunedTuneSessionCount > 0
+ && tunedRecordingSessionCount + tunedTuneSessionCount
+ >= input.getTunerCount()) {
+ for (TvViewSession session : mTvViewSessions) {
+ if (session.mTuned
+ && Objects.equals(session.mInputId, inputId)
+ && !isTunedForRecording(session.mChannelUri)) {
+ session.resetByRecording();
+ break;
}
}
- mChannelUri = channelUri;
- mClient.tune(inputId, channelUri);
}
+ mChannelUri = channelUri;
+ mClient.tune(inputId, channelUri);
});
}
@@ -504,10 +516,10 @@ public class InputSessionManager {
}
}
- private static class DelegateTvInputCallback extends TvInputCallback {
- private final TvInputCallback mDelegate;
+ private static class DelegateTvInputCallback extends TvInputCallbackCompat {
+ private final TvInputCallbackCompat mDelegate;
- DelegateTvInputCallback(TvInputCallback delegate) {
+ DelegateTvInputCallback(TvInputCallbackCompat delegate) {
mDelegate = delegate;
}
@@ -565,6 +577,11 @@ public class InputSessionManager {
public void onTimeShiftStatusChanged(String inputId, int status) {
mDelegate.onTimeShiftStatusChanged(inputId, status);
}
+
+ @Override
+ public void onSignalStrength(String inputId, int value) {
+ mDelegate.onSignalStrength(inputId, value);
+ }
}
/** Called when the {@link TvView} channel is changed. */
diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java
index 94a86cce..b4cf71db 100644
--- a/src/com/android/tv/MainActivity.java
+++ b/src/com/android/tv/MainActivity.java
@@ -22,7 +22,6 @@ import android.app.SearchManager;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
-import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
@@ -49,6 +48,7 @@ import android.provider.Settings;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
@@ -65,16 +65,22 @@ import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
import android.widget.Toast;
+import com.android.tv.MainActivity.MySingletons;
import com.android.tv.analytics.SendChannelStatusRunnable;
import com.android.tv.analytics.SendConfigInfoRunnable;
import com.android.tv.analytics.Tracker;
+import com.android.tv.audio.AudioManagerHelper;
+import com.android.tv.audiotvservice.AudioOnlyTvServiceUtil;
import com.android.tv.common.BuildConfig;
+import com.android.tv.common.CommonConstants;
import com.android.tv.common.CommonPreferences;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.TvContentRatingCache;
import com.android.tv.common.WeakHandler;
+import com.android.tv.common.compat.TvInputInfoCompat;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.memory.MemoryManageable;
+import com.android.tv.common.singletons.HasSingletons;
import com.android.tv.common.ui.setup.OnActionClickListener;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.common.util.ContentUriUtils;
@@ -99,17 +105,19 @@ import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.recorder.ConflictChecker;
import com.android.tv.dvr.ui.DvrStopRecordingFragment;
import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.features.TvFeatures;
import com.android.tv.menu.Menu;
import com.android.tv.onboarding.OnboardingActivity;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
-import com.android.tv.perf.EventNames;
-import com.android.tv.perf.PerformanceMonitor;
-import com.android.tv.perf.TimerEvent;
+import com.android.tv.perf.PerformanceMonitorManagerFactory;
+import com.android.tv.receiver.AudioCapabilitiesReceiver;
import com.android.tv.recommendation.ChannelPreviewUpdater;
import com.android.tv.recommendation.NotificationService;
import com.android.tv.search.ProgramGuideSearchFragment;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
import com.android.tv.ui.ChannelBannerView;
+import com.android.tv.ui.DetailsActivity;
import com.android.tv.ui.InputBannerView;
import com.android.tv.ui.KeypadChannelSwitchView;
import com.android.tv.ui.SelectInputView;
@@ -128,6 +136,7 @@ import com.android.tv.ui.sidepanel.SettingsFragment;
import com.android.tv.ui.sidepanel.SideFragment;
import com.android.tv.ui.sidepanel.parentalcontrols.ParentalControlsFragment;
import com.android.tv.util.AsyncDbTask;
+import com.android.tv.util.AsyncDbTask.DbExecutor;
import com.android.tv.util.CaptionSettings;
import com.android.tv.util.OnboardingUtils;
import com.android.tv.util.RecurringRunner;
@@ -140,6 +149,10 @@ import com.android.tv.util.ViewCache;
import com.android.tv.util.account.AccountHelper;
import com.android.tv.util.images.ImageCache;
+import com.google.common.base.Optional;
+import dagger.android.AndroidInjection;
+import dagger.android.ContributesAndroidInjector;
+import com.android.tv.common.flags.BackendKnobsFlags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
@@ -150,11 +163,21 @@ import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
+import javax.inject.Inject;
+import javax.inject.Provider;
/** The main activity for the Live TV app. */
-public class MainActivity extends Activity implements OnActionClickListener, OnPinCheckedListener {
+public class MainActivity extends Activity
+ implements OnActionClickListener,
+ OnPinCheckedListener,
+ ChannelChanger,
+ HasSingletons<MySingletons> {
private static final String TAG = "MainActivity";
private static final boolean DEBUG = false;
+ private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
+
+ /** Singletons needed for this class. */
+ public interface MySingletons extends ChannelBannerView.MySingletons {}
@Retention(RetentionPolicy.SOURCE)
@IntDef({
@@ -175,6 +198,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
private static final float FRAME_RATE_FOR_FILM = 23.976f;
private static final float FRAME_RATE_EPSILON = 0.1f;
+// AOSP_Comment_Out private static final String PLUTO_TV_PACKAGE_NAME = "tv.pluto.android";
+
private static final int PERMISSIONS_REQUEST_READ_TV_LISTINGS = 1;
private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
@@ -232,10 +257,17 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
private static final int UNDEFINED_TRACK_INDEX = -1;
private static final long START_UP_TIMER_RESET_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(3);
+ {
+ PerformanceMonitorManagerFactory.create().getStartupMeasure().onActivityInit();
+ }
+
+ private final MySingletonsImpl mMySingletons = new MySingletonsImpl();
+ @Inject @DbExecutor Executor mDbExecutor;
+
private AccessibilityManager mAccessibilityManager;
- private ChannelDataManager mChannelDataManager;
- private ProgramDataManager mProgramDataManager;
- private TvInputManagerHelper mTvInputManagerHelper;
+ @Inject ChannelDataManager mChannelDataManager;
+ @Inject ProgramDataManager mProgramDataManager;
+ @Inject TvInputManagerHelper mTvInputManagerHelper;
private ChannelTuner mChannelTuner;
private final TvOptionsManager mTvOptionsManager = new TvOptionsManager(this);
private TvViewUiManager mTvViewUiManager;
@@ -245,10 +277,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
private final DurationTimer mTuneDurationTimer = new DurationTimer();
private DvrManager mDvrManager;
private ConflictChecker mDvrConflictChecker;
- private SetupUtils mSetupUtils;
+ @Inject BackendKnobsFlags mBackendKnobs;
+ @Inject SetupUtils mSetupUtils;
+ @Inject Optional<BuiltInTunerManager> mOptionalBuiltInTunerManager;
+ @VisibleForTesting protected TunableTvView mTvView;
private View mContentView;
- private TunableTvView mTvView;
private Bundle mTuneParams;
@Nullable private Uri mInitChannelUri;
@Nullable private String mParentInputIdWhenScreenOff;
@@ -274,9 +308,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
private boolean mNeedShowBackKeyGuide;
private boolean mVisibleBehind;
private boolean mShowNewSourcesFragment = true;
- private String mTunerInputId;
private boolean mOtherActivityLaunched;
- private PerformanceMonitor mPerformanceMonitor;
private boolean mIsInPIPMode;
private boolean mIsFilmModeSet;
@@ -304,6 +336,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
private RecurringRunner mSendConfigInfoRecurringRunner;
private RecurringRunner mChannelStatusRecurringRunner;
+ private String mLastInputIdFromIntent;
+
private final Handler mHandler = new MainActivityHandler(this);
private final Set<OnActionClickListener> mOnActionClickListeners = new ArraySet<>();
@@ -399,28 +433,27 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
public void onChannelChanged(Channel previousChannel, Channel currentChannel) {}
};
- private final Runnable mRestoreMainViewRunnable =
- new Runnable() {
- @Override
- public void run() {
- restoreMainTvView();
- }
- };
+ private final Runnable mRestoreMainViewRunnable = this::restoreMainTvView;
private ProgramGuideSearchFragment mSearchFragment;
private final TvInputCallback mTvInputCallback =
new TvInputCallback() {
@Override
public void onInputAdded(String inputId) {
- if (TvFeatures.TUNER.isEnabled(MainActivity.this)
- && mTunerInputId.equals(inputId)
+ if (mOptionalBuiltInTunerManager.isPresent()
&& CommonPreferences.shouldShowSetupActivity(MainActivity.this)) {
- Intent intent =
- TvSingletons.getSingletons(MainActivity.this)
- .getTunerSetupIntent(MainActivity.this);
- startActivity(intent);
- CommonPreferences.setShouldShowSetupActivity(MainActivity.this, false);
- mSetupUtils.markAsKnownInput(mTunerInputId);
+ BuiltInTunerManager builtInTunerManager =
+ mOptionalBuiltInTunerManager.get();
+ String tunerInputId = builtInTunerManager.getEmbeddedTunerInputId();
+ if (tunerInputId.equals(inputId)) {
+ Intent intent =
+ builtInTunerManager
+ .getTunerInputController()
+ .createSetupIntent(MainActivity.this);
+ startActivity(intent);
+ CommonPreferences.setShouldShowSetupActivity(MainActivity.this, false);
+ mSetupUtils.markAsKnownInput(tunerInputId);
+ }
}
}
};
@@ -435,12 +468,16 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
@Override
+ public MySingletons singletons() {
+ return mMySingletons;
+ }
+
+ @Override
protected void onCreate(Bundle savedInstanceState) {
+ AndroidInjection.inject(this);
mAccessibilityManager =
(AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
TvSingletons tvSingletons = TvSingletons.getSingletons(this);
- mPerformanceMonitor = tvSingletons.getPerformanceMonitor();
- TimerEvent timer = mPerformanceMonitor.startTimer();
DurationTimer startUpDebugTimer = Debug.getTimer(Debug.TAG_START_UP_TIMER);
if (!startUpDebugTimer.isStarted()
|| startUpDebugTimer.getDuration() > START_UP_TIMER_RESET_THRESHOLD_MS) {
@@ -454,16 +491,13 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
Starter.start(this);
super.onCreate(savedInstanceState);
- if (!tvSingletons.getTvInputManagerHelper().hasTvInputManager()) {
+ if (!mTvInputManagerHelper.hasTvInputManager()) {
Log.wtf(TAG, "Stopping because device does not have a TvInputManager");
finishAndRemoveTask();
return;
}
- mPerformanceMonitor = tvSingletons.getPerformanceMonitor();
- mSetupUtils = tvSingletons.getSetupUtils();
- TvApplication tvApplication = (TvApplication) getApplication();
- mChannelDataManager = tvApplication.getChannelDataManager();
+ TvSingletons tvApplication = (TvSingletons) getApplication();
// In API 23, TvContract.isChannelUriForPassthroughInput is hidden.
boolean isPassthroughInput =
TvContract.isChannelUriForPassthroughInput(getIntent().getData());
@@ -480,17 +514,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
return;
}
setContentView(R.layout.activity_tv);
- mProgramDataManager = tvApplication.getProgramDataManager();
- mTvInputManagerHelper = tvApplication.getTvInputManagerHelper();
mTvView = (TunableTvView) findViewById(R.id.main_tunable_tv_view);
mTvView.initialize(mProgramDataManager, mTvInputManagerHelper);
mTvView.setOnUnhandledInputEventListener(
new OnUnhandledInputEventListener() {
@Override
public boolean onUnhandledInputEvent(InputEvent event) {
- if (DEBUG) {
- Log.d(TAG, "onUnhandledInputEvent " + event);
- }
if (isKeyEventBlocked()) {
return true;
}
@@ -511,7 +540,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
return false;
}
});
- mTvView.setOnTalkBackDpadKeyListener(keycode -> handleUpDownKeys(keycode, null));
+ mTvView.setBlockedInfoOnClickListener(v -> showPinDialogFragment());
long channelId = Utils.getLastWatchedChannelId(this);
String inputId = Utils.getLastWatchedTunerInputId(this);
if (!isPassthroughInput
@@ -525,10 +554,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show();
}
mTracker = tvApplication.getTracker();
- if (TvFeatures.TUNER.isEnabled(this)) {
+ if (mOptionalBuiltInTunerManager.isPresent()) {
mTvInputManagerHelper.addCallback(mTvInputCallback);
}
- mTunerInputId = tvSingletons.getEmbeddedTunerInputId();
mProgramDataManager.addOnCurrentProgramUpdatedListener(
Channel.INVALID_ID, mOnCurrentProgramUpdatedListener);
mProgramDataManager.setPrefetchEnabled(true);
@@ -657,6 +685,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mAccessibilityManager.addAccessibilityStateChangeListener(mOverlayManager);
mAudioManagerHelper = new AudioManagerHelper(this, mTvView);
+ mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, null);
+ mAudioCapabilitiesReceiver.register();
Intent nowPlayingIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent =
PendingIntent.getActivity(this, REQUEST_CODE_NOW_PLAYING, nowPlayingIntent, 0);
@@ -687,7 +717,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
initForTest();
Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onCreate end");
- mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONCREATE);
}
private void startOnboardingActivity() {
@@ -778,7 +807,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
@Override
protected void onStart() {
- TimerEvent timer = mPerformanceMonitor.startTimer();
if (DEBUG) {
Log.d(TAG, "onStart()");
}
@@ -796,15 +824,17 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION);
startService(notificationIntent);
}
- TvSingletons singletons = TvSingletons.getSingletons(this);
- singletons.getTunerInputController().executeNetworkTunerDiscoveryAsyncTask(this);
- singletons.getEpgFetcher().fetchImmediatelyIfNeeded();
- mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONSTART);
+ if (mOptionalBuiltInTunerManager.isPresent()) {
+ mOptionalBuiltInTunerManager
+ .get()
+ .getTunerInputController()
+ .executeNetworkTunerDiscoveryAsyncTask(this);
+ }
+ TvSingletons.getSingletons(this).getEpgFetcher().fetchImmediatelyIfNeeded();
}
@Override
protected void onResume() {
- TimerEvent timer = mPerformanceMonitor.startTimer();
Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume start");
if (DEBUG) Log.d(TAG, "onResume()");
super.onResume();
@@ -836,13 +866,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
getApplicationContext(), TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE)
&& !failedScheduledRecordingInfoSet.isEmpty()) {
runAfterAttachedToWindow(
- new Runnable() {
- @Override
- public void run() {
+ () ->
DvrUiHelper.showDvrInsufficientSpaceErrorDialog(
- MainActivity.this, failedScheduledRecordingInfoSet);
- }
- });
+ MainActivity.this, failedScheduledRecordingInfoSet));
}
if (mChannelTuner.areAllChannelsLoaded()) {
@@ -861,32 +887,23 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
// This will delay the start of the animation until after the Live Channel app is
// shown. Without this the animation is completed before it is actually visible on
// the screen.
- mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- mOverlayManager.showProgramGuide();
- }
- });
+ mHandler.post(() -> mOverlayManager.showProgramGuide());
} else if (mShowSelectInputView) {
mShowSelectInputView = false;
// mShowSelectInputView is true when the activity is started/resumed because the
// TV_INPUT button was pressed in a different app. This will delay the start of
// the animation until after the Live Channel app is shown. Without this the
// animation is completed before it is actually visible on the screen.
- mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- mOverlayManager.showSelectInputView();
- }
- });
+ mHandler.post(() -> mOverlayManager.showSelectInputView());
}
if (mDvrConflictChecker != null) {
mDvrConflictChecker.start();
}
+ if (CommonFeatures.ENABLE_TV_SERVICE.isEnabled(this) && isAudioOnlyInput()) {
+ // TODO(b/110969180): figure out when to call AudioOnlyTvServiceUtil.stopAudioOnlyInput
+ AudioOnlyTvServiceUtil.startAudioOnlyInput(this, mLastInputIdFromIntent);
+ }
Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume end");
- mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONRESUME);
}
@Override
@@ -913,7 +930,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
} else {
mTracker.sendScreenView(SCREEN_BEHIND_NAME);
}
- TvSingletons.getSingletons(this).getExperimentLoader().asyncRefreshExperiments(this);
super.onPause();
}
@@ -1068,6 +1084,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
markCurrentChannelDuringScreenOff();
}
}
+ if (mChannelTuner.isCurrentChannelPassthrough()) {
+ mInitChannelUri = mChannelTuner.getCurrentChannelUri();
+ }
mActivityStarted = false;
stopAll(false);
unregisterReceiver(mBroadcastReceiver);
@@ -1299,19 +1318,15 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
if (!Objects.equals(mTvView.getCurrentChannel(), returnChannel)) {
final Channel channel = returnChannel;
Runnable tuneAction =
- new Runnable() {
- @Override
- public void run() {
- tuneToChannel(channel);
- if (mChannelBeforeShrunkenTvView == null
- || !mChannelBeforeShrunkenTvView.equals(channel)) {
- Utils.setLastWatchedChannel(MainActivity.this, channel);
- }
- mIsCompletingShrunkenTvView = false;
- mIsCurrentChannelUnblockedByUser =
- mWasChannelUnblockedBeforeShrunkenByUser;
- mTvView.setBlockScreenType(getDesiredBlockScreenType());
+ () -> {
+ tuneToChannel(channel);
+ if (mChannelBeforeShrunkenTvView == null
+ || !mChannelBeforeShrunkenTvView.equals(channel)) {
+ Utils.setLastWatchedChannel(MainActivity.this, channel);
}
+ mIsCompletingShrunkenTvView = false;
+ mIsCurrentChannelUnblockedByUser = mWasChannelUnblockedBeforeShrunkenByUser;
+ mTvView.setBlockScreenType(getDesiredBlockScreenType());
};
mTvViewUiManager.fadeOutTvView(tuneAction);
// Will automatically fade-in when video becomes available.
@@ -1423,17 +1438,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
/** Notifies the key input focus is changed to the TV view. */
public void updateKeyInputFocus() {
- mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- mTvView.setBlockScreenType(getDesiredBlockScreenType());
- }
- });
+ mHandler.post(() -> mTvView.setBlockScreenType(getDesiredBlockScreenType()));
}
// It should be called before onResume.
private boolean handleIntent(Intent intent) {
+ mLastInputIdFromIntent = getInputId(intent);
// Reset the closed caption settings when the activity is 1)created or 2) restarted.
// And do not reset while TvView is playing.
if (!mTvView.isPlaying()) {
@@ -1455,13 +1465,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
if (TvInputManager.ACTION_SETUP_INPUTS.equals(intent.getAction())) {
- runAfterAttachedToWindow(
- new Runnable() {
- @Override
- public void run() {
- mOverlayManager.showSetupFragment();
- }
- });
+ runAfterAttachedToWindow(() -> mOverlayManager.showSetupFragment());
} else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
if (Utils.isProgramsUri(uri)) {
@@ -1497,8 +1501,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
long channelIdFromIntent = ContentUriUtils.safeParseId(mInitChannelUri);
if (programUriFromIntent != null && channelIdFromIntent != Channel.INVALID_ID) {
new AsyncQueryProgramTask(
- TvSingletons.getSingletons(this).getDbExecutor(),
- getContentResolver(),
+ mDbExecutor,
programUriFromIntent,
Program.PROJECTION,
null,
@@ -1565,14 +1568,13 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
public AsyncQueryProgramTask(
Executor executor,
- ContentResolver contentResolver,
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String orderBy,
long channelId) {
- super(executor, contentResolver, uri, projection, selection, selectionArgs, orderBy);
+ super(executor, MainActivity.this, uri, projection, selection, selectionArgs, orderBy);
mChannelIdFromIntent = channelId;
}
@@ -1593,26 +1595,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
Channel channel = mChannelDataManager.getChannel(mChannelIdFromIntent);
if (channel != null) {
- ScheduledRecording scheduledRecording =
- TvSingletons.getSingletons(MainActivity.this)
- .getDvrDataManager()
- .getScheduledRecordingForProgramId(program.getId());
- DvrUiHelper.checkStorageStatusAndShowErrorMessage(
- MainActivity.this,
- channel.getInputId(),
- new Runnable() {
- @Override
- public void run() {
- if (CommonFeatures.DVR.isEnabled(MainActivity.this)
- && scheduledRecording == null
- && mDvrManager.isProgramRecordable(program)) {
- DvrUiHelper.requestRecordingFutureProgram(
- MainActivity.this, program, false);
- } else {
- DvrUiHelper.showProgramInfoDialog(MainActivity.this, program);
- }
- }
- });
+ Intent intent = new Intent(MainActivity.this, DetailsActivity.class);
+ intent.putExtra(DetailsActivity.CHANNEL_ID, mChannelIdFromIntent);
+ intent.putExtra(DetailsActivity.DETAILS_VIEW_TYPE, DetailsActivity.PROGRAM_VIEW);
+ intent.putExtra(DetailsActivity.PROGRAM, program);
+ intent.putExtra(DetailsActivity.INPUT_ID, channel.getInputId());
+ startActivity(intent);
}
}
}
@@ -1671,6 +1659,11 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
return;
}
mTunePending = false;
+ if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(this)) {
+ mTvView.resetChannelSignalStrength();
+ mOverlayManager.updateChannelBannerAndShowIfNeeded(
+ TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH);
+ }
final Channel channel = mChannelTuner.getCurrentChannel();
SoftPreconditions.checkState(channel != null);
if (channel == null) {
@@ -1717,18 +1710,14 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
&& mSetupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) {
// Show new channel sources fragment.
runAfterAttachedToWindow(
- new Runnable() {
- @Override
- public void run() {
+ () ->
mOverlayManager.runAfterOverlaysAreClosed(
new Runnable() {
@Override
public void run() {
mOverlayManager.showNewSourcesFragment();
}
- });
- }
- });
+ }));
}
mSetupUtils.onTuned();
if (mTuneParams != null) {
@@ -1799,12 +1788,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
// should be closed when the activity is paused.
private void runAfterAttachedToWindow(final Runnable runnable) {
final Runnable runOnlyIfActivityIsResumed =
- new Runnable() {
- @Override
- public void run() {
- if (mActivityResumed) {
- runnable.run();
- }
+ () -> {
+ if (mActivityResumed) {
+ runnable.run();
}
};
if (mContentView.isAttachedToWindow()) {
@@ -1918,25 +1904,36 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
window.setAttributes(layoutParams);
}
- private void applyMultiAudio() {
+ @VisibleForTesting
+ protected void applyMultiAudio(String trackId) {
List<TvTrackInfo> tracks = getTracks(TvTrackInfo.TYPE_AUDIO);
if (tracks == null) {
mTvOptionsManager.onMultiAudioChanged(null);
return;
}
- String id = TvSettings.getMultiAudioId(this);
- String language = TvSettings.getMultiAudioLanguage(this);
- int channelCount = TvSettings.getMultiAudioChannelCount(this);
- TvTrackInfo bestTrack =
- TvTrackInfoUtils.getBestTrackInfo(tracks, id, language, channelCount);
+ TvTrackInfo bestTrack = null;
+ if (trackId != null) {
+ for (TvTrackInfo track : tracks) {
+ if (trackId.equals(track.getId())) {
+ bestTrack = track;
+ break;
+ }
+ }
+ }
+ if (bestTrack == null) {
+ String id = TvSettings.getMultiAudioId(this);
+ String language = TvSettings.getMultiAudioLanguage(this);
+ int channelCount = TvSettings.getMultiAudioChannelCount(this);
+ bestTrack = TvTrackInfoUtils.getBestTrackInfo(tracks, id, language, channelCount);
+ }
if (bestTrack != null) {
String selectedTrack = getSelectedTrack(TvTrackInfo.TYPE_AUDIO);
if (!bestTrack.getId().equals(selectedTrack)) {
selectTrack(TvTrackInfo.TYPE_AUDIO, bestTrack, UNDEFINED_TRACK_INDEX);
} else {
mTvOptionsManager.onMultiAudioChanged(
- Utils.getMultiAudioString(this, bestTrack, false));
+ TvTrackInfoUtils.getMultiAudioString(this, bestTrack, false));
}
return;
}
@@ -2056,8 +2053,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
if (mMediaSessionWrapper != null) {
mMediaSessionWrapper.release();
}
- if (mAudioManagerHelper != null) {
- mAudioManagerHelper.release();
+ if (mAudioCapabilitiesReceiver != null) {
+ mAudioCapabilitiesReceiver.unregister();
}
mHandler.removeCallbacksAndMessages(null);
application.getMainActivityWrapper().onMainActivityDestroyed(this);
@@ -2071,7 +2068,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
if (mTvInputManagerHelper != null) {
mTvInputManagerHelper.clearTvInputLabels();
- if (TvFeatures.TUNER.isEnabled(this)) {
+ if (mOptionalBuiltInTunerManager.isPresent()) {
mTvInputManagerHelper.removeCallback(mTvInputCallback);
}
}
@@ -2100,51 +2097,59 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
if (!mChannelTuner.areAllChannelsLoaded()) {
return false;
}
- if (handleUpDownKeys(keyCode, event)) {
- return true;
- }
- return super.onKeyDown(keyCode, event);
- }
-
- private boolean handleUpDownKeys(int keyCode, @Nullable KeyEvent event) {
if (!mChannelTuner.isCurrentChannelPassthrough()) {
switch (keyCode) {
case KeyEvent.KEYCODE_CHANNEL_UP:
case KeyEvent.KEYCODE_DPAD_UP:
- if ((event == null || event.getRepeatCount() == 0)
+ if (event.getRepeatCount() == 0
&& mChannelTuner.getBrowsableChannelCount() > 0) {
- // message sending should be done before moving channel, because we use the
- // existence of message to decide if users are switching channel.
- if (event != null) {
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(
- MSG_CHANNEL_UP_PRESSED, System.currentTimeMillis()),
- CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
- }
- moveToAdjacentChannel(true, false);
- mTracker.sendChannelUp();
+
+ channelUpPressed();
}
return true;
case KeyEvent.KEYCODE_CHANNEL_DOWN:
case KeyEvent.KEYCODE_DPAD_DOWN:
- if ((event == null || event.getRepeatCount() == 0)
+ if (event.getRepeatCount() == 0
&& mChannelTuner.getBrowsableChannelCount() > 0) {
- // message sending should be done before moving channel, because we use the
- // existence of message to decide if users are switching channel.
- if (event != null) {
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(
- MSG_CHANNEL_DOWN_PRESSED, System.currentTimeMillis()),
- CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
- }
- moveToAdjacentChannel(false, false);
- mTracker.sendChannelDown();
+ channelDownPressed();
}
return true;
default: // fall out
}
}
- return false;
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public void channelDown() {
+ channelDownPressed();
+ finishChannelChangeIfNeeded();
+ }
+
+ private void channelDownPressed() {
+ // message sending should be done before moving channel, because we use the
+ // existence of message to decide if users are switching channel.
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_CHANNEL_DOWN_PRESSED, System.currentTimeMillis()),
+ CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
+ moveToAdjacentChannel(false, false);
+ mTracker.sendChannelDown();
+ }
+
+ @Override
+ public void channelUp() {
+ channelUpPressed();
+ finishChannelChangeIfNeeded();
+ }
+
+ private void channelUpPressed() {
+ // message sending should be done before moving channel, because we use the
+ // existence of message to decide if users are switching channel.
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_CHANNEL_UP_PRESSED, System.currentTimeMillis()),
+ CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
+ moveToAdjacentChannel(true, false);
+ mTracker.sendChannelUp();
}
@Override
@@ -2228,24 +2233,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
this, mChannelTuner.getCurrentChannel());
return true;
}
- if (!PermissionUtils.hasModifyParentalControls(this)) {
- return true;
- }
- PinDialogFragment dialog = null;
- if (mTvView.isScreenBlocked()) {
- dialog =
- PinDialogFragment.create(
- PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL);
- } else if (mTvView.isContentBlocked()) {
- dialog =
- PinDialogFragment.create(
- PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM,
- mTvView.getBlockedContentRating().flattenToString());
- }
- if (dialog != null) {
- mOverlayManager.showDialogFragment(
- PinDialogFragment.DIALOG_TAG, dialog, false);
- }
+ showPinDialogFragment();
return true;
case KeyEvent.KEYCODE_WINDOW:
enterPictureInPictureMode();
@@ -2315,16 +2303,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
DvrUiHelper.checkStorageStatusAndShowErrorMessage(
this,
currentChannel.getInputId(),
- new Runnable() {
- @Override
- public void run() {
+ () ->
DvrUiHelper.requestRecordingCurrentProgram(
MainActivity.this,
currentChannel,
program,
- false);
- }
- });
+ false));
}
} else {
DvrUiHelper.showStopRecordingDialog(
@@ -2391,6 +2375,24 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
return super.onKeyUp(keyCode, event);
}
+ private void showPinDialogFragment() {
+ if (!PermissionUtils.hasModifyParentalControls(this)) {
+ return;
+ }
+ PinDialogFragment dialog = null;
+ if (mTvView.isScreenBlocked()) {
+ dialog = PinDialogFragment.create(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL);
+ } else if (mTvView.isContentBlocked()) {
+ dialog =
+ PinDialogFragment.create(
+ PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM,
+ mTvView.getBlockedContentRating().flattenToString());
+ }
+ if (dialog != null) {
+ mOverlayManager.showDialogFragment(PinDialogFragment.DIALOG_TAG, dialog, false);
+ }
+ }
+
@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
if (SystemProperties.LOG_KEYEVENT.getValue()) Log.d(TAG, "onKeyLongPress(" + event);
@@ -2423,13 +2425,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mIsInPIPMode = true;
if (mOverlayManager.isOverlayOpened()) {
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION);
- mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- MainActivity.super.enterPictureInPictureMode();
- }
- });
+ mHandler.post(MainActivity.super::enterPictureInPictureMode);
} else {
MainActivity.super.enterPictureInPictureMode();
}
@@ -2586,7 +2582,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mTvView.selectTrack(type, track == null ? null : track.getId());
if (type == TvTrackInfo.TYPE_AUDIO) {
mTvOptionsManager.onMultiAudioChanged(
- track == null ? null : Utils.getMultiAudioString(this, track, false));
+ track == null
+ ? null
+ : TvTrackInfoUtils.getMultiAudioString(this, track, false));
} else if (type == TvTrackInfo.TYPE_SUBTITLE) {
mTvOptionsManager.onClosedCaptionsChanged(track, trackIndex);
}
@@ -2594,7 +2592,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
public void selectAudioTrack(String trackId) {
saveMultiAudioSetting(trackId);
- applyMultiAudio();
+ applyMultiAudio(trackId);
}
private void saveMultiAudioSetting(String trackId) {
@@ -2657,6 +2655,13 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
return;
+ case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED:
+ Toast.makeText(
+ this,
+ R.string.msg_channel_unavailable_not_connected,
+ Toast.LENGTH_SHORT)
+ .show();
+ break;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
default:
Toast.makeText(this, R.string.msg_channel_unavailable_unknown, Toast.LENGTH_SHORT)
@@ -2725,14 +2730,11 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mLazyInitialized = true;
// Running initialization.
mHandler.postDelayed(
- new Runnable() {
- @Override
- public void run() {
- if (mActivityStarted) {
- initAnimations();
- initSideFragments();
- initMenuItemViews();
- }
+ () -> {
+ if (mActivityStarted) {
+ initAnimations();
+ initSideFragments();
+ initMenuItemViews();
}
},
LAZY_INITIALIZATION_DELAY);
@@ -2751,6 +2753,23 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mOverlayManager.getMenu().preloadItemViews();
}
+ private boolean isAudioOnlyInput() {
+ if (mLastInputIdFromIntent == null) {
+ return false;
+ }
+ TvInputInfoCompat inputInfo =
+ mTvInputManagerHelper.getTvInputInfoCompat(mLastInputIdFromIntent);
+ return inputInfo != null && inputInfo.isAudioOnly();
+ }
+
+ @Nullable
+ private String getInputId(Intent intent) {
+ Uri uri = intent.getData();
+ return TvContract.isChannelUriForPassthroughInput(uri)
+ ? uri.getPathSegments().get(1)
+ : null;
+ }
+
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
@@ -2793,15 +2812,22 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
}
- private class MyOnTuneListener implements OnTuneListener {
+ /** {@link OnTuneListener} implementation */
+ @VisibleForTesting
+ protected class MyOnTuneListener implements OnTuneListener {
boolean mUnlockAllowedRatingBeforeShrunken = true;
boolean mWasUnderShrunkenTvView;
Channel mChannel;
- private void onTune(Channel channel, boolean wasUnderShrukenTvView) {
+ private void onTune(Channel channel, boolean wasUnderShrunkenTvView) {
Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.MyOnTuneListener.onTune");
mChannel = channel;
- mWasUnderShrunkenTvView = wasUnderShrukenTvView;
+ mWasUnderShrunkenTvView = wasUnderShrunkenTvView;
+
+ if (mBackendKnobs.enablePartialProgramFetch()) {
+ // Fetch complete projection of tuned channel.
+ mProgramDataManager.prefetchChannel(channel.getId());
+ }
}
@Override
@@ -2824,7 +2850,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
@Override
- public void onStreamInfoChanged(StreamInfo info) {
+ public void onStreamInfoChanged(StreamInfo info, boolean allowAutoSelectionOfTrack) {
if (info.isVideoAvailable() && mTuneDurationTimer.isRunning()) {
mTracker.sendChannelTuneTime(info.getCurrentChannel(), mTuneDurationTimer.reset());
}
@@ -2834,7 +2860,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
applyDisplayRefreshRate(info.getVideoFrameRate());
mTvViewUiManager.updateTvAspectRatio();
- applyMultiAudio();
+ applyMultiAudio(
+ allowAutoSelectionOfTrack ? null : getSelectedTrack(TvTrackInfo.TYPE_AUDIO));
applyClosedCaption();
mOverlayManager.getMenu().onStreamInfoChanged();
if (mTvView.isVideoAvailable()) {
@@ -2861,6 +2888,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
+ channel);
return;
}
+ /* Begin_AOSP_Comment_Out
+ if (PLUTO_TV_PACKAGE_NAME.equals(currentChannel.getPackageName())) {
+ // Do nothing for the Pluto TV input because it misuses this API. b/22720711.
+ return;
+ }
+ End_AOSP_Comment_Out */
if (isChannelChangeKeyDownReceived()) {
// Ignore this message if the user is changing the channel.
return;
@@ -2883,7 +2916,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
// before.
if (mWasUnderShrunkenTvView
&& mUnlockAllowedRatingBeforeShrunken
- && mChannelBeforeShrunkenTvView.equals(mChannel)
+ && Objects.equals(mChannelBeforeShrunkenTvView, mChannel)
&& rating.equals(mAllowedRatingBeforeShrunken)) {
mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView();
mTvView.unblockContent(rating);
@@ -2901,5 +2934,53 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
mOverlayManager.setBlockingContentRating(null);
mMediaSessionWrapper.update(false, getCurrentChannel(), getCurrentProgram());
}
+
+ @Override
+ public void onChannelSignalStrength() {
+ if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(getApplicationContext())) {
+ mOverlayManager.updateChannelBannerAndShowIfNeeded(
+ TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH);
+ }
+ }
+ }
+
+ private class MySingletonsImpl implements MySingletons {
+
+ @Override
+ public Provider<Channel> getCurrentChannelProvider() {
+ return MainActivity.this::getCurrentChannel;
+ }
+
+ @Override
+ public Provider<Program> getCurrentProgramProvider() {
+ return MainActivity.this::getCurrentProgram;
+ }
+
+ @Override
+ public Provider<TvOverlayManager> getOverlayManagerProvider() {
+ return MainActivity.this::getOverlayManager;
+ }
+
+ @Override
+ public TvInputManagerHelper getTvInputManagerHelperSingleton() {
+ return getTvInputManagerHelper();
+ }
+
+ @Override
+ public Provider<Long> getCurrentPlayingPositionProvider() {
+ return MainActivity.this::getCurrentPlayingPosition;
+ }
+
+ @Override
+ public DvrManager getDvrManagerSingleton() {
+ return TvSingletons.getSingletons(getApplicationContext()).getDvrManager();
+ }
+ }
+
+ /** Exports {@link MainActivity} for Dagger codegen to create the appropriate injector. */
+ @dagger.Module
+ public abstract static class Module {
+ @ContributesAndroidInjector
+ abstract MainActivity contributesMainActivityActivityInjector();
}
}
diff --git a/src/com/android/tv/MediaSessionWrapper.java b/src/com/android/tv/MediaSessionWrapper.java
index 43cd74dd..a647a06f 100644
--- a/src/com/android/tv/MediaSessionWrapper.java
+++ b/src/com/android/tv/MediaSessionWrapper.java
@@ -16,12 +16,14 @@
package com.android.tv;
+import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MediaMetadata;
+import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.media.tv.TvContract;
@@ -31,6 +33,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
+import android.util.Log;
import com.android.tv.data.Program;
import com.android.tv.data.api.Channel;
import com.android.tv.util.Utils;
@@ -41,9 +44,12 @@ import com.android.tv.util.images.ImageLoader;
* {@link MainActivity}.
*/
class MediaSessionWrapper {
+ private static final String TAG = "MediaSessionWrapper";
+ private static final boolean DEBUG = false;
private static final String MEDIA_SESSION_TAG = "com.android.tv.mediasession";
- private static final PlaybackState MEDIA_SESSION_STATE_PLAYING =
+ @VisibleForTesting
+ static final PlaybackState MEDIA_SESSION_STATE_PLAYING =
new PlaybackState.Builder()
.setState(
PlaybackState.STATE_PLAYING,
@@ -51,7 +57,8 @@ class MediaSessionWrapper {
1.0f)
.build();
- private static final PlaybackState MEDIA_SESSION_STATE_STOPPED =
+ @VisibleForTesting
+ static final PlaybackState MEDIA_SESSION_STATE_STOPPED =
new PlaybackState.Builder()
.setState(
PlaybackState.STATE_STOPPED,
@@ -61,6 +68,20 @@ class MediaSessionWrapper {
private final Context mContext;
private final MediaSession mMediaSession;
+ private final MediaController.Callback mMediaControllerCallback =
+ new MediaController.Callback() {
+ @Override
+ public void onPlaybackStateChanged(@Nullable PlaybackState state) {
+ super.onPlaybackStateChanged(state);
+ if (DEBUG) {
+ Log.d(TAG, "onPlaybackStateChanged: " + state);
+ }
+ if (isMediaSessionStateStop(state)) {
+ mMediaSession.setActive(false);
+ }
+ }
+ };
+ private MediaController mMediaController;
private int mNowPlayingCardWidth;
private int mNowPlayingCardHeight;
@@ -79,6 +100,8 @@ class MediaSessionWrapper {
MediaSession.FLAG_HANDLES_MEDIA_BUTTONS
| MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
mMediaSession.setSessionActivity(pendingIntent);
+
+ initMediaController();
mNowPlayingCardWidth =
mContext.getResources().getDimensionPixelSize(R.dimen.notif_card_img_max_width);
mNowPlayingCardHeight =
@@ -97,7 +120,6 @@ class MediaSessionWrapper {
mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_PLAYING);
} else if (mMediaSession.isActive()) {
mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_STOPPED);
- mMediaSession.setActive(false);
}
}
@@ -150,6 +172,7 @@ class MediaSessionWrapper {
* @see MediaSession#release()
*/
void release() {
+ unregisterMediaControllerCallback();
mMediaSession.release();
}
@@ -223,6 +246,30 @@ class MediaSessionWrapper {
return mMediaSession;
}
+ @VisibleForTesting
+ MediaController.Callback getMediaControllerCallback() {
+ return mMediaControllerCallback;
+ }
+
+ @VisibleForTesting
+ void initMediaController() {
+ mMediaController = new MediaController(mContext, mMediaSession.getSessionToken());
+ ((Activity) mContext).setMediaController(mMediaController);
+ mMediaController.registerCallback(mMediaControllerCallback);
+ }
+
+ @VisibleForTesting
+ void unregisterMediaControllerCallback() {
+ mMediaController.unregisterCallback(mMediaControllerCallback);
+ }
+
+ private static boolean isMediaSessionStateStop(PlaybackState state) {
+ return state != null
+ && state.getState() == MEDIA_SESSION_STATE_STOPPED.getState()
+ && state.getPosition() == MEDIA_SESSION_STATE_STOPPED.getPosition()
+ && state.getPlaybackSpeed() == MEDIA_SESSION_STATE_STOPPED.getPlaybackSpeed();
+ }
+
private static class ProgramPosterArtCallback
extends ImageLoader.ImageLoaderCallback<MediaSessionWrapper> {
private final Channel mChannel;
diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java
index 199ea51d..5185b122 100644
--- a/src/com/android/tv/SetupPassthroughActivity.java
+++ b/src/com/android/tv/SetupPassthroughActivity.java
@@ -28,11 +28,11 @@ import android.support.annotation.MainThread;
import android.util.Log;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.actions.InputSetupActionUtils;
-import com.android.tv.common.experiments.Experiments;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.ChannelDataManager.Listener;
import com.android.tv.data.epg.EpgFetcher;
import com.android.tv.data.epg.EpgInputWhiteList;
+import com.android.tv.features.TvFeatures;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -66,12 +66,10 @@ public class SetupPassthroughActivity extends Activity {
Intent intent = getIntent();
String inputId = intent.getStringExtra(InputSetupActionUtils.EXTRA_INPUT_ID);
mTvInputInfo = inputManager.getTvInputInfo(inputId);
- mEpgInputWhiteList = new EpgInputWhiteList(tvSingletons.getRemoteConfig());
+ mEpgInputWhiteList = new EpgInputWhiteList(tvSingletons.getCloudEpgFlags());
mActivityAfterCompletion = InputSetupActionUtils.getExtraActivityAfter(intent);
boolean needToFetchEpg =
- mTvInputInfo != null
- && Utils.isInternalTvInput(this, mTvInputInfo.getId())
- && Experiments.CLOUD_EPG.get();
+ mTvInputInfo != null && Utils.isInternalTvInput(this, mTvInputInfo.getId());
if (needToFetchEpg) {
// In case when the activity is restored, this flag should be restored as well.
mEpgFetcherDuringScan = true;
@@ -144,23 +142,30 @@ public class SetupPassthroughActivity extends Activity {
finish();
return;
}
+ if (mTvInputInfo == null) {
+ Log.w(
+ TAG,
+ "There is no input with ID "
+ + getIntent().getStringExtra(InputSetupActionUtils.EXTRA_INPUT_ID)
+ + ".");
+ setResult(resultCode, data);
+ finish();
+ return;
+ }
TvSingletons.getSingletons(this)
.getSetupUtils()
.onTvInputSetupFinished(
mTvInputInfo.getId(),
- new Runnable() {
- @Override
- public void run() {
- if (mActivityAfterCompletion != null) {
- try {
- startActivity(mActivityAfterCompletion);
- } catch (ActivityNotFoundException e) {
- Log.w(TAG, "Activity launch failed", e);
- }
+ () -> {
+ if (mActivityAfterCompletion != null) {
+ try {
+ startActivity(mActivityAfterCompletion);
+ } catch (ActivityNotFoundException e) {
+ Log.w(TAG, "Activity launch failed", e);
}
- setResult(resultCode, data);
- finish();
}
+ setResult(resultCode, data);
+ finish();
});
}
@@ -178,15 +183,12 @@ public class SetupPassthroughActivity extends Activity {
private final ChannelDataManager mChannelDataManager;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final Runnable mScanTimeoutRunnable =
- new Runnable() {
- @Override
- public void run() {
- Log.w(
- TAG,
- "No channels has been added for a while."
- + " The scan might have finished unexpectedly.");
- onScanTimedOut();
- }
+ () -> {
+ Log.w(
+ TAG,
+ "No channels has been added for a while."
+ + " The scan might have finished unexpectedly.");
+ onScanTimedOut();
};
private final Listener mChannelDataManagerListener =
new Listener() {
diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java
index bb3574d7..779e8df6 100644
--- a/src/com/android/tv/TimeShiftManager.java
+++ b/src/com/android/tv/TimeShiftManager.java
@@ -17,7 +17,6 @@
package com.android.tv;
import android.annotation.SuppressLint;
-import android.content.ContentResolver;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
@@ -35,7 +34,7 @@ import com.android.tv.data.Program;
import com.android.tv.data.ProgramDataManager;
import com.android.tv.data.api.Channel;
import com.android.tv.ui.TunableTvView;
-import com.android.tv.ui.TunableTvViewPlayingApi.TimeShiftListener;
+import com.android.tv.ui.api.TunableTvViewPlayingApi.TimeShiftListener;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.TimeShiftUtils;
import com.android.tv.util.Utils;
@@ -87,16 +86,15 @@ public class TimeShiftManager {
@Retention(RetentionPolicy.SOURCE)
@IntDef(
- flag = true,
- value = {
- TIME_SHIFT_ACTION_ID_PLAY,
- TIME_SHIFT_ACTION_ID_PAUSE,
- TIME_SHIFT_ACTION_ID_REWIND,
- TIME_SHIFT_ACTION_ID_FAST_FORWARD,
- TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS,
- TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT
- }
- )
+ flag = true,
+ value = {
+ TIME_SHIFT_ACTION_ID_PLAY,
+ TIME_SHIFT_ACTION_ID_PAUSE,
+ TIME_SHIFT_ACTION_ID_REWIND,
+ TIME_SHIFT_ACTION_ID_FAST_FORWARD,
+ TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS,
+ TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT
+ })
public @interface TimeShiftActionId {}
public static final int TIME_SHIFT_ACTION_ID_PLAY = 1;
@@ -715,7 +713,7 @@ public class TimeShiftManager {
: mRecordEndTimeMs;
long currentPositionMs =
Math.max(
- Math.min(mTvView.timeshiftGetCurrentPositionMs(), currentTimeMs),
+ Math.min(mTvView.timeShiftGetCurrentPositionMs(), currentTimeMs),
mRecordStartTimeMs);
boolean isCurrentTime =
currentTimeMs - currentPositionMs < RECORDING_BOUNDARY_THRESHOLD;
@@ -723,7 +721,7 @@ public class TimeShiftManager {
if (isCurrentTime && isForwarding()) {
// It's playing forward and the current playing position reached
// the current system time. i.e. The live stream is played.
- // Therefore no need to call TvView.timeshiftGetCurrentPositionMs
+ // Therefore no need to call TvView.timeShiftGetCurrentPositionMs
// any more.
newCurrentPositionMs = currentTimeMs;
mIsPlayOffsetChanged = false;
@@ -753,14 +751,14 @@ public class TimeShiftManager {
mDisplayedPlaySpeed = PLAY_SPEED_1X;
mPlaybackSpeed = 1;
mPlayDirection = PLAY_DIRECTION_FORWARD;
- mTvView.timeshiftPlay();
+ mTvView.timeShiftPlay();
setPlayStatus(PLAY_STATUS_PLAYING);
}
void pause() {
mDisplayedPlaySpeed = PLAY_SPEED_1X;
mPlaybackSpeed = 1;
- mTvView.timeshiftPause();
+ mTvView.timeShiftPause();
setPlayStatus(PLAY_STATUS_PAUSED);
mIsPlayOffsetChanged = true;
}
@@ -783,7 +781,7 @@ public class TimeShiftManager {
}
mPlayDirection = PLAY_DIRECTION_BACKWARD;
mPlaybackSpeed = getPlaybackSpeed();
- mTvView.timeshiftRewind(mPlaybackSpeed);
+ mTvView.timeShiftRewind(mPlaybackSpeed);
setPlayStatus(PLAY_STATUS_PLAYING);
mIsPlayOffsetChanged = true;
}
@@ -796,14 +794,14 @@ public class TimeShiftManager {
}
mPlayDirection = PLAY_DIRECTION_FORWARD;
mPlaybackSpeed = getPlaybackSpeed();
- mTvView.timeshiftFastForward(mPlaybackSpeed);
+ mTvView.timeShiftFastForward(mPlaybackSpeed);
setPlayStatus(PLAY_STATUS_PLAYING);
mIsPlayOffsetChanged = true;
}
/** Moves to the specified time. */
void seekTo(long timeMs) {
- mTvView.timeshiftSeekTo(
+ mTvView.timeShiftSeekTo(
Math.min(
mRecordEndTimeMs == CURRENT_TIME
? System.currentTimeMillis()
@@ -821,9 +819,9 @@ public class TimeShiftManager {
if (playbackSpeed != mPlaybackSpeed) {
mPlaybackSpeed = playbackSpeed;
if (mPlayDirection == PLAY_DIRECTION_FORWARD) {
- mTvView.timeshiftFastForward(mPlaybackSpeed);
+ mTvView.timeShiftFastForward(mPlaybackSpeed);
} else {
- mTvView.timeshiftRewind(mPlaybackSpeed);
+ mTvView.timeShiftRewind(mPlaybackSpeed);
}
}
}
@@ -977,8 +975,7 @@ public class TimeShiftManager {
}
}
if (mChannel != null) {
- mProgramLoadTask =
- new LoadProgramsForCurrentChannelTask(mContext.getContentResolver(), next);
+ mProgramLoadTask = new LoadProgramsForCurrentChannelTask(next);
mProgramLoadTask.executeOnDbThread();
}
}
@@ -1225,10 +1222,10 @@ public class TimeShiftManager {
private class LoadProgramsForCurrentChannelTask
extends AsyncDbTask.LoadProgramsForChannelTask {
- LoadProgramsForCurrentChannelTask(ContentResolver contentResolver, Range<Long> period) {
+ LoadProgramsForCurrentChannelTask(Range<Long> period) {
super(
TvSingletons.getSingletons(mContext).getDbExecutor(),
- contentResolver,
+ mContext,
mChannel.getId(),
period);
}
@@ -1309,13 +1306,7 @@ public class TimeShiftManager {
mProgramLoadTask = null;
}
// Need to post to handler, because the task is still running.
- mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- startTaskIfNeeded();
- }
- });
+ mHandler.post(ProgramManager.this::startTaskIfNeeded);
}
boolean overlaps(Queue<Range<Long>> programLoadQueue) {
diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java
index 826317b9..5f25a24b 100644
--- a/src/com/android/tv/TvApplication.java
+++ b/src/com/android/tv/TvApplication.java
@@ -34,8 +34,8 @@ import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
+import android.widget.Toast;
import com.android.tv.common.BaseApplication;
-import com.android.tv.common.concurrent.NamedThreadFactory;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.recording.RecordingStorageStatusManager;
import com.android.tv.common.ui.setup.animation.SetupAnimationHelper;
@@ -55,17 +55,22 @@ import com.android.tv.dvr.DvrStorageStatusManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
import com.android.tv.dvr.recorder.RecordingScheduler;
import com.android.tv.dvr.ui.browse.DvrBrowseActivity;
+import com.android.tv.features.TvFeatures;
+import com.android.tv.perf.PerformanceMonitorManager;
+import com.android.tv.perf.PerformanceMonitorManagerFactory;
import com.android.tv.recommendation.ChannelPreviewUpdater;
import com.android.tv.recommendation.RecordedProgramPreviewUpdater;
-import com.android.tv.tuner.TunerInputController;
-import com.android.tv.tuner.util.TunerInputInfoUtils;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
+import com.android.tv.tunerinputcontroller.TunerInputController;
+import com.android.tv.util.AsyncDbTask.DbExecutor;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
+import com.google.common.base.Optional;
+import dagger.Lazy;
import java.util.List;
import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import javax.inject.Inject;
/**
* Live TV application.
@@ -73,6 +78,9 @@ import java.util.concurrent.Executors;
* <p>This includes all the Google specific hooks.
*/
public abstract class TvApplication extends BaseApplication implements TvSingletons, Starter {
+
+ protected static final PerformanceMonitorManager PERFORMANCE_MONITOR_MANAGER =
+ PerformanceMonitorManagerFactory.create();
private static final String TAG = "TvApplication";
private static final boolean DEBUG = false;
@@ -89,10 +97,6 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
private static final String PREFERENCE_IS_FIRST_LAUNCH = "is_first_launch";
- private static final NamedThreadFactory THREAD_FACTORY = new NamedThreadFactory("tv-app-db");
- private static final ExecutorService DB_EXECUTOR =
- Executors.newSingleThreadExecutor(THREAD_FACTORY);
-
private String mVersionName = "";
private final MainActivityWrapper mMainActivityWrapper = new MainActivityWrapper();
@@ -111,22 +115,28 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
// STOP-SHIP: Remove this variable when Tuner Process is split to another application.
// When this variable is null, we don't know in which process TvApplication runs.
private Boolean mRunningInMainProcess;
- private TvInputManagerHelper mTvInputManagerHelper;
+ @Inject Lazy<TvInputManagerHelper> mLazyTvInputManagerHelper;
private boolean mStarted;
private EpgFetcher mEpgFetcher;
- private TunerInputController mTunerInputController;
+
+ @Inject Optional<BuiltInTunerManager> mOptionalBuiltInTunerManager;
+ @Inject SetupUtils mSetupUtils;
+ @Inject @DbExecutor Executor mDbExecutor;
@Override
public void onCreate() {
+ if (getSystemService(TvInputManager.class) == null) {
+ String msg = "Not an Android TV device.";
+ Toast.makeText(this, msg, Toast.LENGTH_LONG);
+ Log.wtf(TAG, msg);
+ throw new IllegalStateException(msg);
+ }
super.onCreate();
SharedPreferencesUtils.initialize(
this,
- new Runnable() {
- @Override
- public void run() {
- if (mRunningInMainProcess != null && mRunningInMainProcess) {
- checkTunerServiceOnFirstLaunch();
- }
+ () -> {
+ if (mRunningInMainProcess != null && mRunningInMainProcess) {
+ checkTunerServiceOnFirstLaunch();
}
});
try {
@@ -164,13 +174,19 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
new TvInputCallback() {
@Override
public void onInputAdded(String inputId) {
- if (TvFeatures.TUNER.isEnabled(TvApplication.this)
- && TextUtils.equals(
- inputId, getEmbeddedTunerInputId())) {
- TunerInputInfoUtils.updateTunerInputInfo(
- TvApplication.this);
+ if (mOptionalBuiltInTunerManager.isPresent()) {
+ BuiltInTunerManager builtInTunerManager =
+ mOptionalBuiltInTunerManager.get();
+ if (TextUtils.equals(
+ inputId,
+ builtInTunerManager.getEmbeddedTunerInputId())) {
+
+ builtInTunerManager
+ .getTunerInputController()
+ .updateTunerInputInfo(TvApplication.this);
+ }
+ handleInputCountChanged();
}
- handleInputCountChanged();
}
@Override
@@ -178,10 +194,13 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
handleInputCountChanged();
}
});
- if (TvFeatures.TUNER.isEnabled(this)) {
+ if (mOptionalBuiltInTunerManager.isPresent()) {
// If the tuner input service is added before the app is started, we need to
// handle it here.
- TunerInputInfoUtils.updateTunerInputInfo(TvApplication.this);
+ mOptionalBuiltInTunerManager
+ .get()
+ .getTunerInputController()
+ .updateTunerInputInfo(TvApplication.this);
}
if (CommonFeatures.DVR.isEnabled(this)) {
mDvrScheduleManager = new DvrScheduleManager(this);
@@ -205,8 +224,12 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
boolean isFirstLaunch = sharedPreferences.getBoolean(PREFERENCE_IS_FIRST_LAUNCH, true);
if (isFirstLaunch) {
if (DEBUG) Log.d(TAG, "Congratulations, it's the first launch!");
- getTunerInputController()
- .onCheckingUsbTunerStatus(this, ACTION_APPLICATION_FIRST_LAUNCHED);
+ if (mOptionalBuiltInTunerManager.isPresent()) {
+ mOptionalBuiltInTunerManager
+ .get()
+ .getTunerInputController()
+ .onCheckingUsbTunerStatus(this, ACTION_APPLICATION_FIRST_LAUNCHED);
+ }
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(PREFERENCE_IS_FIRST_LAUNCH, false);
editor.apply();
@@ -220,7 +243,7 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
@Override
public synchronized SetupUtils getSetupUtils() {
- return SetupUtils.createForTvSingletons(this);
+ return mSetupUtils;
}
/** Returns the {@link DvrManager}. */
@@ -282,13 +305,10 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
return mProgramDataManager;
}
Utils.runInMainThreadAndWait(
- new Runnable() {
- @Override
- public void run() {
- if (mProgramDataManager == null) {
- mProgramDataManager = new ProgramDataManager(TvApplication.this);
- mProgramDataManager.start();
- }
+ () -> {
+ if (mProgramDataManager == null) {
+ mProgramDataManager = new ProgramDataManager(TvApplication.this);
+ mProgramDataManager.start();
}
});
return mProgramDataManager;
@@ -340,21 +360,7 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
/** Returns {@link TvInputManagerHelper}. */
@Override
public TvInputManagerHelper getTvInputManagerHelper() {
- if (mTvInputManagerHelper == null) {
- mTvInputManagerHelper = new TvInputManagerHelper(this);
- mTvInputManagerHelper.start();
- }
- return mTvInputManagerHelper;
- }
-
- @Override
- public synchronized TunerInputController getTunerInputController() {
- if (mTunerInputController == null) {
- mTunerInputController =
- new TunerInputController(
- ComponentName.unflattenFromString(getEmbeddedTunerInputId()));
- }
- return mTunerInputController;
+ return mLazyTvInputManagerHelper.get();
}
@Override
@@ -480,12 +486,16 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
if (!enable) {
List<TvInputInfo> inputs = inputManager.getTvInputList();
boolean skipTunerInputCheck = false;
+ Optional<String> optionalEmbeddedTunerInputId =
+ mOptionalBuiltInTunerManager.transform(
+ BuiltInTunerManager::getEmbeddedTunerInputId);
// Enable the TvActivity only if there is at least one tuner type input.
if (!skipTunerInputCheck) {
for (TvInputInfo input : inputs) {
if (calledByTunerServiceChanged
&& !tunerServiceEnabled
- && getEmbeddedTunerInputId().equals(input.getId())) {
+ && optionalEmbeddedTunerInputId.isPresent()
+ && optionalEmbeddedTunerInputId.get().equals(input.getId())) {
continue;
}
if (input.getType() == TvInputInfo.TYPE_TUNER) {
@@ -507,11 +517,11 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
name, newState, dontKillApp ? PackageManager.DONT_KILL_APP : 0);
Log.i(TAG, (enable ? "Un-hide" : "Hide") + " Live TV.");
}
- getSetupUtils().onInputListUpdated(inputManager);
+ mSetupUtils.onInputListUpdated(inputManager);
}
@Override
public Executor getDbExecutor() {
- return DB_EXECUTOR;
+ return mDbExecutor;
}
}
diff --git a/src/com/android/tv/TvSingletons.java b/src/com/android/tv/TvSingletons.java
index 0c7f78a3..20edf3d4 100644
--- a/src/com/android/tv/TvSingletons.java
+++ b/src/com/android/tv/TvSingletons.java
@@ -22,6 +22,7 @@ import com.android.tv.analytics.Tracker;
import com.android.tv.common.BaseApplication;
import com.android.tv.common.BaseSingletons;
import com.android.tv.common.experiments.ExperimentLoader;
+import com.android.tv.common.flags.has.HasUiFlags;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.PreviewDataManager;
import com.android.tv.data.ProgramDataManager;
@@ -33,17 +34,23 @@ import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
import com.android.tv.dvr.recorder.RecordingScheduler;
import com.android.tv.perf.PerformanceMonitor;
-import com.android.tv.tuner.TunerInputController;
+import com.android.tv.tunerinputcontroller.HasBuiltInTunerManager;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.account.AccountHelper;
+import com.android.tv.common.flags.BackendKnobsFlags;
import java.util.concurrent.Executor;
import javax.inject.Provider;
/** Interface with getters for application scoped singletons. */
-public interface TvSingletons extends BaseSingletons {
+public interface TvSingletons extends BaseSingletons, HasBuiltInTunerManager, HasUiFlags {
- /** Returns the @{@link TvSingletons} using the application context. */
+ /**
+ * Returns the @{@link TvSingletons} using the application context.
+ *
+ * @deprecated use injection instead.
+ */
+ @Deprecated
static TvSingletons getSingletons(Context context) {
return (TvSingletons) BaseApplication.getSingletons(context);
}
@@ -52,6 +59,7 @@ public interface TvSingletons extends BaseSingletons {
void handleInputCountChanged();
+ @Deprecated
ChannelDataManager getChannelDataManager();
/**
@@ -60,6 +68,8 @@ public interface TvSingletons extends BaseSingletons {
*/
boolean isChannelDataManagerLoadFinished();
+ /** @deprecated use injection instead. */
+ @Deprecated
ProgramDataManager getProgramDataManager();
/**
@@ -92,17 +102,23 @@ public interface TvSingletons extends BaseSingletons {
PerformanceMonitor getPerformanceMonitor();
+ /** @deprecated use injection instead. */
+ @Deprecated
TvInputManagerHelper getTvInputManagerHelper();
Provider<EpgReader> providesEpgReader();
EpgFetcher getEpgFetcher();
+ /** @deprecated use injection instead. */
+ @Deprecated
SetupUtils getSetupUtils();
- TunerInputController getTunerInputController();
-
ExperimentLoader getExperimentLoader();
+ /** @deprecated use injection instead. */
+ @Deprecated
Executor getDbExecutor();
+
+ BackendKnobsFlags getBackendKnobs();
}
diff --git a/src/com/android/tv/analytics/SendChannelStatusRunnable.java b/src/com/android/tv/analytics/SendChannelStatusRunnable.java
index 4a84434c..306bd855 100644
--- a/src/com/android/tv/analytics/SendChannelStatusRunnable.java
+++ b/src/com/android/tv/analytics/SendChannelStatusRunnable.java
@@ -43,13 +43,7 @@ public class SendChannelStatusRunnable implements Runnable {
final SendChannelStatusRunnable sendChannelStatusRunnable =
new SendChannelStatusRunnable(channelDataManager, tracker);
- Runnable onStopRunnable =
- new Runnable() {
- @Override
- public void run() {
- sendChannelStatusRunnable.setDbLoadListener(null);
- }
- };
+ Runnable onStopRunnable = () -> sendChannelStatusRunnable.setDbLoadListener(null);
final RecurringRunner recurringRunner =
new RecurringRunner(
context,
@@ -70,14 +64,7 @@ public class SendChannelStatusRunnable implements Runnable {
// done
// via a post on the main thread
new Handler(Looper.getMainLooper())
- .post(
- new Runnable() {
- @Override
- public void run() {
- sendChannelStatusRunnable.setDbLoadListener(
- null);
- }
- });
+ .post(() -> sendChannelStatusRunnable.setDbLoadListener(null));
recurringRunner.start();
}
diff --git a/src/com/android/tv/app/LiveTvApplication.java b/src/com/android/tv/app/LiveTvApplication.java
index 461331d5..38e85e48 100644
--- a/src/com/android/tv/app/LiveTvApplication.java
+++ b/src/com/android/tv/app/LiveTvApplication.java
@@ -16,36 +16,37 @@
package com.android.tv.app;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.media.tv.TvContract;
import com.android.tv.TvApplication;
+import com.android.tv.TvSingletons;
import com.android.tv.analytics.Analytics;
import com.android.tv.analytics.StubAnalytics;
import com.android.tv.analytics.Tracker;
-import com.android.tv.common.CommonConstants;
-import com.android.tv.common.actions.InputSetupActionUtils;
-import com.android.tv.common.config.DefaultConfigManager;
-import com.android.tv.common.config.api.RemoteConfig;
+import com.android.tv.common.dagger.ApplicationModule;
import com.android.tv.common.experiments.ExperimentLoader;
-import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.flags.impl.DefaultBackendKnobsFlags;
+import com.android.tv.common.flags.impl.DefaultCloudEpgFlags;
+import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags;
+import com.android.tv.common.flags.impl.DefaultUiFlags;
+import com.android.tv.common.singletons.HasSingletons;
import com.android.tv.data.epg.EpgReader;
import com.android.tv.data.epg.StubEpgReader;
+import com.android.tv.modules.TvSingletonsModule;
import com.android.tv.perf.PerformanceMonitor;
-import com.android.tv.perf.StubPerformanceMonitor;
-import com.android.tv.tuner.livetuner.LiveTvTunerTvInputService;
-import com.android.tv.tuner.setup.LiveTvTunerSetupActivity;
+import com.android.tv.perf.PerformanceMonitorManagerFactory;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
import com.android.tv.util.account.AccountHelper;
import com.android.tv.util.account.AccountHelperImpl;
+import com.google.common.base.Optional;
+import dagger.android.AndroidInjector;
import javax.inject.Provider;
/** The top level application for Live TV. */
-public class LiveTvApplication extends TvApplication {
- protected static final String TV_ACTIVITY_CLASS_NAME =
- CommonConstants.BASE_PACKAGE + ".TvActivity";
+public class LiveTvApplication extends TvApplication implements HasSingletons<TvSingletons> {
+
+ static {
+ PERFORMANCE_MONITOR_MANAGER.getStartupMeasure().onAppClassLoaded();
+ }
- private final StubPerformanceMonitor performanceMonitor = new StubPerformanceMonitor();
private final Provider<EpgReader> mEpgReaderProvider =
new Provider<EpgReader>() {
@@ -55,12 +56,30 @@ public class LiveTvApplication extends TvApplication {
}
};
+ private final DefaultBackendKnobsFlags mBackendKnobsFlags = new DefaultBackendKnobsFlags();
+ private final DefaultCloudEpgFlags mCloudEpgFlags = new DefaultCloudEpgFlags();
+ private final DefaultUiFlags mUiFlags = new DefaultUiFlags();
+ private final DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags =
+ new DefaultConcurrentDvrPlaybackFlags();
private AccountHelper mAccountHelper;
private Analytics mAnalytics;
private Tracker mTracker;
- private String mEmbeddedInputId;
- private RemoteConfig mRemoteConfig;
private ExperimentLoader mExperimentLoader;
+ private PerformanceMonitor mPerformanceMonitor;
+
+ @Override
+ protected AndroidInjector<LiveTvApplication> applicationInjector() {
+ return DaggerLiveTvApplicationComponent.builder()
+ .applicationModule(new ApplicationModule(this))
+ .tvSingletonsModule(new TvSingletonsModule(this))
+ .build();
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ PERFORMANCE_MONITOR_MANAGER.getStartupMeasure().onAppCreate(this);
+ }
/** Returns the {@link AccountHelperImpl}. */
@Override
@@ -73,7 +92,10 @@ public class LiveTvApplication extends TvApplication {
@Override
public synchronized PerformanceMonitor getPerformanceMonitor() {
- return performanceMonitor;
+ if (mPerformanceMonitor == null) {
+ mPerformanceMonitor = PerformanceMonitorManagerFactory.create().initialize(this);
+ }
+ return mPerformanceMonitor;
}
@Override
@@ -87,6 +109,11 @@ public class LiveTvApplication extends TvApplication {
return mExperimentLoader;
}
+ @Override
+ public DefaultBackendKnobsFlags getBackendKnobs() {
+ return mBackendKnobsFlags;
+ }
+
/** Returns the {@link Analytics}. */
@Override
public synchronized Analytics getAnalytics() {
@@ -106,34 +133,32 @@ public class LiveTvApplication extends TvApplication {
}
@Override
- public Intent getTunerSetupIntent(Context context) {
- // Make an intent to launch the setup activity of TV tuner input.
- Intent intent =
- CommonUtils.createSetupIntent(
- new Intent(context, LiveTvTunerSetupActivity.class), mEmbeddedInputId);
- intent.putExtra(InputSetupActionUtils.EXTRA_INPUT_ID, mEmbeddedInputId);
- Intent tvActivityIntent = new Intent();
- tvActivityIntent.setComponent(new ComponentName(context, TV_ACTIVITY_CLASS_NAME));
- intent.putExtra(InputSetupActionUtils.EXTRA_ACTIVITY_AFTER_COMPLETION, tvActivityIntent);
- return intent;
+ public DefaultCloudEpgFlags getCloudEpgFlags() {
+ return mCloudEpgFlags;
}
@Override
- public synchronized String getEmbeddedTunerInputId() {
- if (mEmbeddedInputId == null) {
- mEmbeddedInputId =
- TvContract.buildInputId(
- new ComponentName(this, LiveTvTunerTvInputService.class));
- }
- return mEmbeddedInputId;
+ public DefaultUiFlags getUiFlags() {
+ return mUiFlags;
}
@Override
- public RemoteConfig getRemoteConfig() {
- if (mRemoteConfig == null) {
- // No need to synchronize this, it does not hurt to create two and throw one away.
- mRemoteConfig = DefaultConfigManager.createInstance(this).getRemoteConfig();
- }
- return mRemoteConfig;
+ public Optional<BuiltInTunerManager> getBuiltInTunerManager() {
+ return Optional.absent();
+ }
+
+ @Override
+ public BuildType getBuildType() {
+ return BuildType.AOSP;
+ }
+
+ @Override
+ public DefaultConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() {
+ return mConcurrentDvrPlaybackFlags;
+ }
+
+ @Override
+ public TvSingletons singletons() {
+ return this;
}
}
diff --git a/src/com/android/tv/app/LiveTvApplicationComponent.java b/src/com/android/tv/app/LiveTvApplicationComponent.java
new file mode 100644
index 00000000..3d3f0492
--- /dev/null
+++ b/src/com/android/tv/app/LiveTvApplicationComponent.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2018 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.app;
+
+import dagger.Component;
+import dagger.android.AndroidInjectionModule;
+import dagger.android.AndroidInjector;
+import javax.inject.Singleton;
+
+/** Dagger component for {@link LiveTvApplication}. */
+@Singleton
+@Component(modules = {AndroidInjectionModule.class, LiveTvModule.class})
+public interface LiveTvApplicationComponent extends AndroidInjector<LiveTvApplication> {}
diff --git a/src/com/android/tv/app/LiveTvModule.java b/src/com/android/tv/app/LiveTvModule.java
new file mode 100644
index 00000000..a28749bd
--- /dev/null
+++ b/src/com/android/tv/app/LiveTvModule.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 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.app;
+
+import com.android.tv.common.flags.impl.DefaultFlagsModule;
+import com.android.tv.modules.TvApplicationModule;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
+import com.google.common.base.Optional;
+import dagger.Module;
+import dagger.Provides;
+
+/** Dagger module for {@link LiveTvApplication}. */
+@Module(includes = {DefaultFlagsModule.class, TvApplicationModule.class})
+class LiveTvModule {
+
+ @Provides
+ Optional<BuiltInTunerManager> providesBuiltInTunerManager() {
+ return Optional.absent();
+ }
+}
diff --git a/src/com/android/tv/AudioManagerHelper.java b/src/com/android/tv/audio/AudioManagerHelper.java
index 942d431d..4acff2d3 100644
--- a/src/com/android/tv/AudioManagerHelper.java
+++ b/src/com/android/tv/audio/AudioManagerHelper.java
@@ -13,18 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.tv;
+package com.android.tv.audio;
import android.app.Activity;
import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Build;
-import com.android.tv.receiver.AudioCapabilitiesReceiver;
-import com.android.tv.ui.TunableTvView;
-import com.android.tv.ui.TunableTvViewPlayingApi;
+import android.support.annotation.Nullable;
+import com.android.tv.features.TvFeatures;
+import com.android.tv.ui.api.TunableTvViewPlayingApi;
-/** A helper class to help {@link MainActivity} to handle audio-related stuffs. */
-class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener {
+/** A helper class to help {@code Activities} to handle audio-related stuffs. */
+public class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener {
private static final float AUDIO_MAX_VOLUME = 1.0f;
private static final float AUDIO_MIN_VOLUME = 0.0f;
private static final float AUDIO_DUCKING_VOLUME = 0.3f;
@@ -32,42 +34,53 @@ class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener {
private final Activity mActivity;
private final TunableTvViewPlayingApi mTvView;
private final AudioManager mAudioManager;
- private final AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
+ @Nullable private final AudioFocusRequest mFocusRequest;
- private boolean mAc3PassthroughSupported;
- private int mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
+ private int mAudioFocusStatus = AudioManager.AUDIOFOCUS_NONE;
- AudioManagerHelper(Activity activity, TunableTvViewPlayingApi tvView) {
+ public AudioManagerHelper(Activity activity, TunableTvViewPlayingApi tvView) {
mActivity = activity;
mTvView = tvView;
mAudioManager = (AudioManager) activity.getSystemService(Context.AUDIO_SERVICE);
- mAudioCapabilitiesReceiver =
- new AudioCapabilitiesReceiver(
- activity,
- new AudioCapabilitiesReceiver.OnAc3PassthroughCapabilityChangeListener() {
- @Override
- public void onAc3PassthroughCapabilityChange(boolean capability) {
- mAc3PassthroughSupported = capability;
- }
- });
- mAudioCapabilitiesReceiver.register();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ mFocusRequest =
+ new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
+ .setAudioAttributes(
+ new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build())
+ .setOnAudioFocusChangeListener(this)
+ // Auto ducking from the system does not mute the TV Input Service.
+ // Using will pause when ducked allows us to set the stream volume
+ // even when we are not pausing.
+ .setWillPauseWhenDucked(true)
+ .build();
+ } else {
+ mFocusRequest = null;
+ }
}
/**
- * Sets suitable volume to {@link TunableTvView} according to the current audio focus. If the
- * focus status is {@link AudioManager#AUDIOFOCUS_LOSS} and the activity is under PIP mode, this
- * method will finish the activity.
+ * Sets suitable volume to {@link TunableTvViewPlayingApi} according to the current audio focus.
+ *
+ * <p>If the focus status is {@link AudioManager#AUDIOFOCUS_LOSS} or {@link
+ * AudioManager#AUDIOFOCUS_NONE} and the activity is under PIP mode, this method will finish the
+ * activity. Sets suitable volume to {@link TunableTvViewPlayingApi} according to the current
+ * audio focus. If the focus status is {@link AudioManager#AUDIOFOCUS_LOSS} and the activity is
+ * under PIP mode, this method will finish the activity.
*/
- void setVolumeByAudioFocusStatus() {
+ public void setVolumeByAudioFocusStatus() {
if (mTvView.isPlaying()) {
switch (mAudioFocusStatus) {
case AudioManager.AUDIOFOCUS_GAIN:
if (mTvView.isTimeShiftAvailable()) {
- mTvView.timeshiftPlay();
+ mTvView.timeShiftPlay();
} else {
mTvView.setStreamVolume(AUDIO_MAX_VOLUME);
}
break;
+ case AudioManager.AUDIOFOCUS_NONE:
case AudioManager.AUDIOFOCUS_LOSS:
if (TvFeatures.PICTURE_IN_PICTURE.isEnabled(mActivity)
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
@@ -78,14 +91,14 @@ class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener {
// fall through
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
if (mTvView.isTimeShiftAvailable()) {
- mTvView.timeshiftPause();
+ mTvView.timeShiftPause();
} else {
mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
}
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
if (mTvView.isTimeShiftAvailable()) {
- mTvView.timeshiftPause();
+ mTvView.timeShiftPause();
} else {
mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME);
}
@@ -98,10 +111,15 @@ class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener {
* Tries to request audio focus from {@link AudioManager} and set volume according to the
* returned result.
*/
- void requestAudioFocus() {
- int result =
- mAudioManager.requestAudioFocus(
- this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+ public void requestAudioFocus() {
+ int result;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ result = mAudioManager.requestAudioFocus(mFocusRequest);
+ } else {
+ result =
+ mAudioManager.requestAudioFocus(
+ this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+ }
mAudioFocusStatus =
(result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
? AudioManager.AUDIOFOCUS_GAIN
@@ -110,19 +128,13 @@ class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener {
}
/** Abandons audio focus. */
- void abandonAudioFocus() {
+ public void abandonAudioFocus() {
mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
- mAudioManager.abandonAudioFocus(this);
- }
-
- /** Returns {@code true} if the device supports AC3 pass-through. */
- boolean isAc3PassthroughSupported() {
- return mAc3PassthroughSupported;
- }
-
- /** Release the resources the helper class may occupied. */
- void release() {
- mAudioCapabilitiesReceiver.unregister();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ mAudioManager.abandonAudioFocusRequest(mFocusRequest);
+ } else {
+ mAudioManager.abandonAudioFocus(this);
+ }
}
@Override
diff --git a/src/com/android/tv/audiotvservice/AudioOnlyTvService.java b/src/com/android/tv/audiotvservice/AudioOnlyTvService.java
new file mode 100644
index 00000000..5d0e9c82
--- /dev/null
+++ b/src/com/android/tv/audiotvservice/AudioOnlyTvService.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2018 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.audiotvservice;
+
+import android.app.Notification;
+import android.app.Service;
+import android.content.Intent;
+import android.media.session.MediaSession;
+import android.net.Uri;
+import android.os.IBinder;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import com.android.tv.data.ChannelImpl;
+import com.android.tv.data.StreamInfo;
+import com.android.tv.data.api.Channel;
+import com.android.tv.ui.TunableTvView;
+import com.android.tv.ui.TunableTvView.OnTuneListener;
+
+/** Foreground service for audio-only TV inputs. */
+public class AudioOnlyTvService extends Service implements OnTuneListener {
+ // TODO(b/110969180): implement this service.
+ private static final String TAG = "AudioOnlyTvService";
+ private static final int NOTIFICATION_ID = 1;
+
+ @Nullable private String mTvInputId;
+ private TunableTvView mTvView;
+ // TODO(b/110969180): perhaps use MediaSessionWrapper
+ private MediaSession mMediaSession;
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ Log.i(TAG, "onBind");
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ Log.i(TAG, "onCreate");
+ // TODO(b/110969180): create TvView
+
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.i(TAG, "onStartCommand. flags = " + flags + ", startId = " + startId);
+ // TODO(b/110969180): real notification and or media session
+ startForeground(NOTIFICATION_ID, new Notification());
+ mTvInputId = AudioOnlyTvServiceUtil.getInputIdFromIntent(intent);
+ tune(mTvInputId);
+ return START_STICKY;
+ }
+
+ private void tune(String tvInputId) {
+ Channel channel = ChannelImpl.createPassthroughChannel(tvInputId);
+ mTvView.tuneTo(channel, null, this);
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.i(TAG, "onDestroy");
+ mTvInputId = null;
+ // TODO(b/110969180): clear TvView
+ }
+
+ // TODO(b/110969180): figure out when to stop ourselves, mediaSession event?
+
+ // TODO(b/110969180): handle OnTuner Listener
+ @Override
+ public void onTuneFailed(Channel channel) {}
+
+ @Override
+ public void onUnexpectedStop(Channel channel) {}
+
+ @Override
+ public void onStreamInfoChanged(StreamInfo info, boolean allowAutoSelectionOfTrack) {}
+
+ @Override
+ public void onChannelRetuned(Uri channel) {}
+
+ @Override
+ public void onContentBlocked() {}
+
+ @Override
+ public void onContentAllowed() {}
+
+ @Override
+ public void onChannelSignalStrength() {}
+}
diff --git a/src/com/android/tv/audiotvservice/AudioOnlyTvServiceUtil.java b/src/com/android/tv/audiotvservice/AudioOnlyTvServiceUtil.java
new file mode 100644
index 00000000..7ffe8833
--- /dev/null
+++ b/src/com/android/tv/audiotvservice/AudioOnlyTvServiceUtil.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2018 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.audiotvservice;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+/** Utility methods to start and stop audio only TV Player. */
+public final class AudioOnlyTvServiceUtil {
+ private static final String TAG = "AudioOnlyTvServiceUtil";
+ private static final String EXTRA_INPUT_ID = "intputId";
+
+ @MainThread
+ public static void startAudioOnlyInput(Context context, String tvInputId) {
+ Log.i(TAG, "startAudioOnlyInput");
+ Intent intent = getIntent(context);
+ if (intent == null) {
+ return;
+ }
+ intent.putExtra(EXTRA_INPUT_ID, tvInputId);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(intent);
+ } else {
+ context.startService(intent);
+ }
+ }
+
+ @Nullable
+ private static Intent getIntent(Context context) {
+ try {
+ return new Intent(
+ context, Class.forName("com.android.tv.audiotvservice.AudioOnlyTvService"));
+ } catch (ClassNotFoundException e) {
+ Log.wtf(TAG, e);
+ return null;
+ }
+ }
+
+ @MainThread
+ public static void stopAudioOnlyInput(Context context) {
+ Log.i(TAG, "stopForegroundService");
+ context.stopService(getIntent(context));
+ }
+
+ @Nullable
+ public static String getInputIdFromIntent(Intent intent) {
+ return intent.getStringExtra(EXTRA_INPUT_ID);
+ }
+
+ private AudioOnlyTvServiceUtil() {}
+}
diff --git a/src/com/android/tv/audiotvservice/README.md b/src/com/android/tv/audiotvservice/README.md
new file mode 100644
index 00000000..0f40ff6c
--- /dev/null
+++ b/src/com/android/tv/audiotvservice/README.md
@@ -0,0 +1,18 @@
+# AudioOnlyTvServiceUtil
+
+This service plays audio only TV inputs in the "background".
+
+
+
+## Usage
+
+To start playing call
+
+```java
+AudioOnlyTvServiceUtil.startAudioOnlyInput(context, tivInputServiceUri);
+```
+To stop the playback call.
+
+```java
+AudioOnlyTvServiceUtil.stopAudioOnlyInput(context);
+``` \ No newline at end of file
diff --git a/src/com/android/tv/data/BaseProgram.java b/src/com/android/tv/data/BaseProgram.java
index 0fb1e58d..9650fd18 100644
--- a/src/com/android/tv/data/BaseProgram.java
+++ b/src/com/android/tv/data/BaseProgram.java
@@ -21,7 +21,9 @@ import android.media.tv.TvContentRating;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.android.tv.R;
+import com.google.common.collect.ImmutableList;
import java.util.Comparator;
+import java.util.Objects;
/**
* Base class for {@link com.android.tv.data.Program} and {@link
@@ -43,6 +45,10 @@ public abstract class BaseProgram {
public static final Comparator<BaseProgram> SEASON_REVERSED_EPISODE_COMPARATOR =
new EpisodeComparator(true);
+ public static final String COLUMN_SERIES_ID = "series_id";
+
+ public static final String COLUMN_STATE = "state";
+
private static class EpisodeComparator implements Comparator<BaseProgram> {
private final boolean mReversedSeason;
@@ -66,7 +72,7 @@ public abstract class BaseProgram {
/** Compares two strings represent season numbers or episode numbers of programs. */
public static int numberCompare(String s1, String s2) {
- if (s1 == s2) {
+ if (Objects.equals(s1, s2)) {
return 0;
} else if (s1 == null) {
return -1;
@@ -92,6 +98,7 @@ public abstract class BaseProgram {
public abstract String getEpisodeTitle();
/** Returns the displayed title of the program episode. */
+ @Nullable
public String getEpisodeDisplayTitle(Context context) {
String episodeNumber = getEpisodeNumber();
String episodeTitle = getEpisodeTitle();
@@ -162,6 +169,7 @@ public abstract class BaseProgram {
public abstract long getDurationMillis();
/** Returns the series ID. */
+ @Nullable
public abstract String getSeriesId();
/** Returns the season number. */
@@ -180,8 +188,7 @@ public abstract class BaseProgram {
public abstract int[] getCanonicalGenreIds();
/** Returns the array of content ratings. */
- @Nullable
- public abstract TvContentRating[] getContentRatings();
+ public abstract ImmutableList<TvContentRating> getContentRatings();
/** Returns channel's ID of the program. */
public abstract long getChannelId();
diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java
index 1dfcf125..a5c786cf 100644
--- a/src/com/android/tv/data/ChannelDataManager.java
+++ b/src/com/android/tv/data/ChannelDataManager.java
@@ -23,7 +23,6 @@ import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.res.AssetFileDescriptor;
import android.database.ContentObserver;
-import android.database.sqlite.SQLiteException;
import android.media.tv.TvContract;
import android.media.tv.TvContract.Channels;
import android.media.tv.TvInputManager.TvInputCallback;
@@ -47,7 +46,7 @@ import com.android.tv.data.api.Channel;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
-import java.io.IOException;
+import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -515,7 +514,7 @@ public class ChannelDataManager {
if (mChannelsUpdateTask != null) {
mChannelsUpdateTask.cancel(true);
}
- mChannelsUpdateTask = new QueryAllChannelsTask(mContentResolver);
+ mChannelsUpdateTask = new QueryAllChannelsTask();
mChannelsUpdateTask.executeOnDbThread();
}
@@ -599,8 +598,10 @@ public class ChannelDataManager {
.openAssetFileDescriptor(
TvContract.buildChannelLogoUri(mChannel.getId()), "r")) {
return true;
- } catch (SQLiteException | IOException | NullPointerException e) {
- // File not found or asset file not found.
+ } catch (FileNotFoundException e) {
+ // no need to log just return false
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to find logo for " + mChannel, e);
}
return false;
}
@@ -616,8 +617,8 @@ public class ChannelDataManager {
private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask {
- QueryAllChannelsTask(ContentResolver contentResolver) {
- super(mDbExecutor, contentResolver);
+ QueryAllChannelsTask() {
+ super(mDbExecutor, mContext);
}
@Override
@@ -736,15 +737,12 @@ public class ChannelDataManager {
return;
}
mDbExecutor.execute(
- new Runnable() {
- @Override
- public void run() {
- String selection = Utils.buildSelectionForIds(Channels._ID, ids);
- ContentValues values = new ContentValues();
- values.put(columnName, columnValue);
- mContentResolver.update(
- TvContract.Channels.CONTENT_URI, values, selection, null);
- }
+ () -> {
+ String selection = Utils.buildSelectionForIds(Channels._ID, ids);
+ ContentValues values = new ContentValues();
+ values.put(columnName, columnValue);
+ mContentResolver.update(
+ TvContract.Channels.CONTENT_URI, values, selection, null);
});
}
diff --git a/src/com/android/tv/data/ChannelImpl.java b/src/com/android/tv/data/ChannelImpl.java
index 703f69c9..f31290d0 100644
--- a/src/com/android/tv/data/ChannelImpl.java
+++ b/src/com/android/tv/data/ChannelImpl.java
@@ -46,12 +46,8 @@ public final class ChannelImpl implements Channel {
/** Compares the channel numbers of channels which belong to the same input. */
public static final Comparator<Channel> CHANNEL_NUMBER_COMPARATOR =
- new Comparator<Channel>() {
- @Override
- public int compare(Channel lhs, Channel rhs) {
- return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber());
- }
- };
+ (Channel lhs, Channel rhs) ->
+ ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber());
private static final int APP_LINK_TYPE_NOT_SET = 0;
private static final String INVALID_PACKAGE_NAME = "packageName";
@@ -74,6 +70,7 @@ public final class ChannelImpl implements Channel {
TvContract.Channels.COLUMN_APP_LINK_ICON_URI,
TvContract.Channels.COLUMN_APP_LINK_POSTER_ART_URI,
TvContract.Channels.COLUMN_APP_LINK_INTENT_URI,
+ TvContract.Channels.COLUMN_NETWORK_AFFILIATION,
TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, // Only used in bundled input
};
@@ -102,6 +99,7 @@ public final class ChannelImpl implements Channel {
channel.mAppLinkIconUri = cursor.getString(index++);
channel.mAppLinkPosterArtUri = cursor.getString(index++);
channel.mAppLinkIntentUri = cursor.getString(index++);
+ channel.mNetworkAffiliation = cursor.getString(index++);
if (CommonUtils.isBundledInput(channel.mInputId)) {
channel.mRecordingProhibited = cursor.getInt(index++) != 0;
}
@@ -146,6 +144,7 @@ public final class ChannelImpl implements Channel {
private String mAppLinkPosterArtUri;
private String mAppLinkIntentUri;
private Intent mAppLinkIntent;
+ private String mNetworkAffiliation;
private int mAppLinkType;
private String mLogoUri;
private boolean mRecordingProhibited;
@@ -247,6 +246,11 @@ public final class ChannelImpl implements Channel {
return mAppLinkIntentUri;
}
+ @Override
+ public String getNetworkAffiliation() {
+ return mNetworkAffiliation;
+ }
+
/** Returns channel logo uri which is got from cloud, it's used only for ChannelLogoFetcher. */
@Override
public String getLogoUri() {
@@ -311,6 +315,11 @@ public final class ChannelImpl implements Channel {
mLogoUri = logoUri;
}
+ @Override
+ public void setNetworkAffiliation(String networkAffiliation) {
+ mNetworkAffiliation = networkAffiliation;
+ }
+
/**
* Check whether {@code other} has same read-only channel info as this. But, it cannot check two
* channels have same logos. It also excludes browsable and locked, because two fields are
@@ -393,8 +402,10 @@ public final class ChannelImpl implements Channel {
mAppLinkIconUri = channel.getAppLinkIconUri();
mAppLinkPosterArtUri = channel.getAppLinkPosterArtUri();
mAppLinkIntentUri = channel.getAppLinkIntentUri();
+ mNetworkAffiliation = channel.getNetworkAffiliation();
mRecordingProhibited = channel.isRecordingProhibited();
mChannelLogoExist = channel.channelLogoExists();
+ mNetworkAffiliation = channel.getNetworkAffiliation();
}
}
@@ -421,6 +432,7 @@ public final class ChannelImpl implements Channel {
mAppLinkIconUri = other.mAppLinkIconUri;
mAppLinkPosterArtUri = other.mAppLinkPosterArtUri;
mAppLinkIntentUri = other.mAppLinkIntentUri;
+ mNetworkAffiliation = channel.mNetworkAffiliation;
mAppLinkIntent = other.mAppLinkIntent;
mAppLinkType = other.mAppLinkType;
mRecordingProhibited = other.mRecordingProhibited;
@@ -543,6 +555,12 @@ public final class ChannelImpl implements Channel {
return this;
}
+ @VisibleForTesting
+ public Builder setNetworkAffiliation(String networkAffiliation) {
+ mChannel.mNetworkAffiliation = networkAffiliation;
+ return this;
+ }
+
public Builder setAppLinkColor(int appLinkColor) {
mChannel.mAppLinkColor = appLinkColor;
return this;
diff --git a/src/com/android/tv/data/PreviewDataManager.java b/src/com/android/tv/data/PreviewDataManager.java
index 44664dcf..8616aeec 100644
--- a/src/com/android/tv/data/PreviewDataManager.java
+++ b/src/com/android/tv/data/PreviewDataManager.java
@@ -21,7 +21,6 @@ import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
-import android.database.SQLException;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
@@ -31,10 +30,10 @@ import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.IntDef;
import android.support.annotation.MainThread;
-import android.support.media.tv.ChannelLogoUtils;
-import android.support.media.tv.PreviewProgram;
import android.util.Log;
import android.util.Pair;
+import androidx.tvprovider.media.tv.ChannelLogoUtils;
+import androidx.tvprovider.media.tv.PreviewProgram;
import com.android.tv.R;
import com.android.tv.common.util.PermissionUtils;
import java.lang.annotation.Retention;
@@ -225,14 +224,14 @@ public class PreviewDataManager {
try (Cursor cursor =
mContentResolver.query(
previewChannelsUri,
- android.support.media.tv.Channel.PROJECTION,
+ androidx.tvprovider.media.tv.Channel.PROJECTION,
mChannelSelection,
new String[] {packageName},
null)) {
if (cursor != null) {
while (cursor.moveToNext()) {
- android.support.media.tv.Channel previewChannel =
- android.support.media.tv.Channel.fromCursor(cursor);
+ androidx.tvprovider.media.tv.Channel previewChannel =
+ androidx.tvprovider.media.tv.Channel.fromCursor(cursor);
Long previewChannelType = previewChannel.getInternalProviderFlag1();
if (previewChannelType != null) {
previewData.addPreviewChannelId(
@@ -245,14 +244,14 @@ public class PreviewDataManager {
try (Cursor cursor =
mContentResolver.query(
previewChannelsUri,
- android.support.media.tv.Channel.PROJECTION,
+ androidx.tvprovider.media.tv.Channel.PROJECTION,
null,
null,
null)) {
if (cursor != null) {
while (cursor.moveToNext()) {
- android.support.media.tv.Channel previewChannel =
- android.support.media.tv.Channel.fromCursor(cursor);
+ androidx.tvprovider.media.tv.Channel previewChannel =
+ androidx.tvprovider.media.tv.Channel.fromCursor(cursor);
Long previewChannelType = previewChannel.getInternalProviderFlag1();
if (packageName.equals(previewChannel.getPackageName())
&& previewChannelType != null) {
@@ -283,7 +282,7 @@ public class PreviewDataManager {
}
}
}
- } catch (SQLException e) {
+ } catch (Exception e) {
Log.w(TAG, "Unable to get preview data", e);
}
return previewData;
@@ -554,7 +553,7 @@ public class PreviewDataManager {
/** A utils class for preview data. */
public static final class PreviewDataUtils {
/** Creates a preview channel. */
- public static android.support.media.tv.Channel createPreviewChannel(
+ public static androidx.tvprovider.media.tv.Channel createPreviewChannel(
Context context, @PreviewChannelType long previewChannelType) {
if (previewChannelType == TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL) {
return createRecordedProgramPreviewChannel(context, previewChannelType);
@@ -562,10 +561,10 @@ public class PreviewDataManager {
return createDefaultPreviewChannel(context, previewChannelType);
}
- private static android.support.media.tv.Channel createDefaultPreviewChannel(
+ private static androidx.tvprovider.media.tv.Channel createDefaultPreviewChannel(
Context context, @PreviewChannelType long previewChannelType) {
- android.support.media.tv.Channel.Builder builder =
- new android.support.media.tv.Channel.Builder();
+ androidx.tvprovider.media.tv.Channel.Builder builder =
+ new androidx.tvprovider.media.tv.Channel.Builder();
CharSequence appLabel =
context.getApplicationInfo().loadLabel(context.getPackageManager());
CharSequence appDescription =
@@ -578,10 +577,10 @@ public class PreviewDataManager {
return builder.build();
}
- private static android.support.media.tv.Channel createRecordedProgramPreviewChannel(
+ private static androidx.tvprovider.media.tv.Channel createRecordedProgramPreviewChannel(
Context context, @PreviewChannelType long previewChannelType) {
- android.support.media.tv.Channel.Builder builder =
- new android.support.media.tv.Channel.Builder();
+ androidx.tvprovider.media.tv.Channel.Builder builder =
+ new androidx.tvprovider.media.tv.Channel.Builder();
builder.setType(TvContract.Channels.TYPE_PREVIEW)
.setDisplayName(
context.getResources()
diff --git a/src/com/android/tv/data/PreviewProgramContent.java b/src/com/android/tv/data/PreviewProgramContent.java
index b5156408..8d4b88cf 100644
--- a/src/com/android/tv/data/PreviewProgramContent.java
+++ b/src/com/android/tv/data/PreviewProgramContent.java
@@ -19,9 +19,9 @@ package com.android.tv.data;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.VisibleForTesting;
-import android.support.media.tv.TvContractCompat;
import android.text.TextUtils;
import android.util.Pair;
+import androidx.tvprovider.media.tv.TvContractCompat;
import com.android.tv.TvSingletons;
import com.android.tv.data.api.Channel;
import com.android.tv.dvr.data.RecordedProgram;
diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/Program.java
index 2c64cdbb..b688927a 100644
--- a/src/com/android/tv/data/Program.java
+++ b/src/com/android/tv/data/Program.java
@@ -30,6 +30,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
import com.android.tv.common.BuildConfig;
@@ -37,8 +38,10 @@ import com.android.tv.common.TvContentRatingCache;
import com.android.tv.common.util.CollectionUtils;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.data.api.Channel;
+import com.android.tv.util.TvProviderUtils;
import com.android.tv.util.Utils;
import com.android.tv.util.images.ImageLoader;
+import com.google.common.collect.ImmutableList;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
@@ -86,6 +89,16 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
public static final String[] PROJECTION = createProjection();
+ public static final String[] PARTIAL_PROJECTION = {
+ TvContract.Programs._ID,
+ TvContract.Programs.COLUMN_CHANNEL_ID,
+ TvContract.Programs.COLUMN_TITLE,
+ TvContract.Programs.COLUMN_EPISODE_TITLE,
+ TvContract.Programs.COLUMN_CANONICAL_GENRE,
+ TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
+ TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
+ };
+
private static String[] createProjection() {
return CollectionUtils.concatAll(
PROJECTION_BASE,
@@ -94,7 +107,10 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
: PROJECTION_DEPRECATED_IN_NYC);
}
- /** Returns the column index for {@code column}, -1 if the column doesn't exist. */
+ /**
+ * Returns the column index for {@code column},-1 if the column doesn't exist in {@link
+ * #PROJECTION}.
+ */
public static int getColumnIndex(String column) {
for (int i = 0; i < PROJECTION.length; ++i) {
if (PROJECTION[i].equals(column)) {
@@ -104,11 +120,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
return -1;
}
- /**
- * Creates {@code Program} object from cursor.
- *
- * <p>The query that created the cursor MUST use {@link #PROJECTION}.
- */
+ /** Creates {@code Program} object from cursor. */
public static Program fromCursor(Cursor cursor) {
// Columns read must match the order of match {@link #PROJECTION}
Builder builder = new Builder();
@@ -143,6 +155,27 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
builder.setSeasonNumber(cursor.getString(index++));
builder.setEpisodeNumber(cursor.getString(index++));
}
+ if (TvProviderUtils.getProgramHasSeriesIdColumn()) {
+ String seriesId = cursor.getString(index);
+ if (!TextUtils.isEmpty(seriesId)) {
+ builder.setSeriesId(seriesId);
+ }
+ }
+ return builder.build();
+ }
+
+ /** Creates {@code Program} object from cursor. */
+ public static Program fromCursorPartialProjection(Cursor cursor) {
+ // Columns read must match the order of match {@link #PARTIAL_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.setCanonicalGenres(cursor.getString(index++));
+ builder.setStartTimeUtcMillis(cursor.getLong(index++));
+ builder.setEndTimeUtcMillis(cursor.getLong(index++));
return builder.build();
}
@@ -169,10 +202,14 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
program.mCanonicalGenreIds = in.createIntArray();
int length = in.readInt();
if (length > 0) {
- program.mContentRatings = new TvContentRating[length];
+ ImmutableList.Builder<TvContentRating> ratingsBuilder =
+ ImmutableList.builderWithExpectedSize(length);
for (int i = 0; i < length; ++i) {
- program.mContentRatings[i] = TvContentRating.unflattenFromString(in.readString());
+ ratingsBuilder.add(TvContentRating.unflattenFromString(in.readString()));
}
+ program.mContentRatings = ratingsBuilder.build();
+ } else {
+ program.mContentRatings = ImmutableList.of();
}
program.mRecordingProhibited = in.readByte() != (byte) 0;
return program;
@@ -202,6 +239,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
private String mEpisodeNumber;
private long mStartTimeUtcMillis;
private long mEndTimeUtcMillis;
+ private String mDurationString;
private String mDescription;
private String mLongDescription;
private int mVideoWidth;
@@ -210,7 +248,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
private String mPosterArtUri;
private String mThumbnailUri;
private int[] mCanonicalGenreIds;
- private TvContentRating[] mContentRatings;
+ private ImmutableList<TvContentRating> mContentRatings;
private boolean mRecordingProhibited;
private Program() {
@@ -278,6 +316,15 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
return mEndTimeUtcMillis;
}
+ public String getDurationString(Context context) {
+ // TODO(b/71717446): expire the calculated string
+ if (mDurationString == null) {
+ mDurationString =
+ Utils.getDurationString(context, mStartTimeUtcMillis, mEndTimeUtcMillis, true);
+ }
+ return mDurationString;
+ }
+
/** Returns the program duration. */
@Override
public long getDurationMillis() {
@@ -310,7 +357,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
@Nullable
@Override
- public TvContentRating[] getContentRatings() {
+ public ImmutableList<TvContentRating> getContentRatings() {
return mContentRatings;
}
@@ -379,7 +426,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
mVideoHeight,
mPosterArtUri,
mThumbnailUri,
- Arrays.hashCode(mContentRatings),
+ mContentRatings,
Arrays.hashCode(mCanonicalGenreIds),
mSeasonNumber,
mSeasonTitle,
@@ -407,7 +454,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
&& mVideoHeight == program.mVideoHeight
&& Objects.equals(mPosterArtUri, program.mPosterArtUri)
&& Objects.equals(mThumbnailUri, program.mThumbnailUri)
- && Arrays.equals(mContentRatings, program.mContentRatings)
+ && Objects.equals(mContentRatings, program.mContentRatings)
&& Arrays.equals(mCanonicalGenreIds, program.mCanonicalGenreIds)
&& Objects.equals(mSeasonNumber, program.mSeasonNumber)
&& Objects.equals(mSeasonTitle, program.mSeasonTitle)
@@ -474,7 +521,8 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
*/
@SuppressLint("InlinedApi")
@SuppressWarnings("deprecation")
- public static ContentValues toContentValues(Program program) {
+ @WorkerThread
+ public static ContentValues toContentValues(Program program, Context context) {
ContentValues values = new ContentValues();
values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId());
if (!TextUtils.isEmpty(program.getPackageName())) {
@@ -495,6 +543,10 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber());
putValue(values, TvContract.Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber());
}
+ if (TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) {
+ putValue(values, COLUMN_SERIES_ID, program.getSeriesId());
+ }
+
putValue(values, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription());
putValue(values, TvContract.Programs.COLUMN_LONG_DESCRIPTION, program.getLongDescription());
putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri());
@@ -554,6 +606,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
mEpisodeNumber = other.mEpisodeNumber;
mStartTimeUtcMillis = other.mStartTimeUtcMillis;
mEndTimeUtcMillis = other.mEndTimeUtcMillis;
+ mDurationString = null; // Recreate Duration when needed.
mDescription = other.mDescription;
mLongDescription = other.mLongDescription;
mVideoWidth = other.mVideoWidth;
@@ -582,6 +635,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
mProgram.mEpisodeNumber = null;
mProgram.mStartTimeUtcMillis = -1;
mProgram.mEndTimeUtcMillis = -1;
+ mProgram.mDurationString = null;
mProgram.mDescription = null;
mProgram.mLongDescription = null;
mProgram.mRecordingProhibited = false;
@@ -771,7 +825,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
* @param contentRatings the content ratings
* @return a reference to this object
*/
- public Builder setContentRatings(TvContentRating[] contentRatings) {
+ public Builder setContentRatings(ImmutableList<TvContentRating> contentRatings) {
mProgram.mContentRatings = contentRatings;
return this;
}
@@ -947,7 +1001,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
out.writeString(mPosterArtUri);
out.writeString(mThumbnailUri);
out.writeIntArray(mCanonicalGenreIds);
- out.writeInt(mContentRatings == null ? 0 : mContentRatings.length);
+ out.writeInt(mContentRatings == null ? 0 : mContentRatings.size());
if (mContentRatings != null) {
for (TvContentRating rating : mContentRatings) {
out.writeString(rating.flattenToString());
diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java
index 4631806c..2f20c89a 100644
--- a/src/com/android/tv/data/ProgramDataManager.java
+++ b/src/com/android/tv/data/ProgramDataManager.java
@@ -35,14 +35,17 @@ import android.util.LongSparseArray;
import android.util.LruCache;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.config.api.RemoteConfig;
-import com.android.tv.common.config.api.RemoteConfigValue;
import com.android.tv.common.memory.MemoryManageable;
import com.android.tv.common.util.Clock;
import com.android.tv.data.api.Channel;
+import com.android.tv.perf.EventNames;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.TimerEvent;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.MultiLongSparseArray;
+import com.android.tv.util.TvProviderUtils;
import com.android.tv.util.Utils;
+import com.android.tv.common.flags.BackendKnobsFlags;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -71,8 +74,6 @@ public class ProgramDataManager implements MemoryManageable {
// TODO: need to optimize consecutive DB updates.
private static final long CURRENT_PROGRAM_UPDATE_WAIT_MS = TimeUnit.SECONDS.toMillis(5);
@VisibleForTesting static final long PROGRAM_GUIDE_SNAP_TIME_MS = TimeUnit.MINUTES.toMillis(30);
- private static final RemoteConfigValue<Long> PROGRAM_GUIDE_MAX_HOURS =
- RemoteConfigValue.create("live_channels_program_guide_max_hours", 48);
// TODO: Use TvContract constants, once they become public.
private static final String PARAM_START_TIME = "start_time";
@@ -90,10 +91,13 @@ public class ProgramDataManager implements MemoryManageable {
private static final int MSG_UPDATE_ONE_CURRENT_PROGRAM = 1001;
private static final int MSG_UPDATE_PREFETCH_PROGRAM = 1002;
+ private final Context mContext;
private final Clock mClock;
private final ContentResolver mContentResolver;
private final Executor mDbExecutor;
- private final RemoteConfig mRemoteConfig;
+ private final BackendKnobsFlags mBackendKnobsFlags;
+ private final PerformanceMonitor mPerformanceMonitor;
+ private final ChannelDataManager mChannelDataManager;
private boolean mStarted;
// Updated only on the main thread.
private volatile boolean mCurrentProgramsLoadFinished;
@@ -104,15 +108,15 @@ public class ProgramDataManager implements MemoryManageable {
private final MultiLongSparseArray<OnCurrentProgramUpdatedListener>
mChannelId2ProgramUpdatedListeners = new MultiLongSparseArray<>();
private final Handler mHandler;
- private final Set<Listener> mListeners = new ArraySet<>();
-
+ private final Set<Callback> mCallbacks = new ArraySet<>();
+ private Map<Long, ArrayList<Program>> mChannelIdProgramCache = new ConcurrentHashMap<>();
+ private final Set<Long> mCompleteInfoChannelIds = new HashSet<>();
private final ContentObserver mProgramObserver;
private boolean mPrefetchEnabled;
private long mProgramPrefetchUpdateWaitMs;
private long mLastPrefetchTaskRunMs;
private ProgramsPrefetchTask mProgramsPrefetchTask;
- private Map<Long, ArrayList<Program>> mChannelIdProgramCache = new HashMap<>();
// Any program that ends prior to this time will be removed from the cache
// when a channel's current program is updated.
@@ -125,25 +129,34 @@ public class ProgramDataManager implements MemoryManageable {
@MainThread
public ProgramDataManager(Context context) {
this(
+ context,
TvSingletons.getSingletons(context).getDbExecutor(),
context.getContentResolver(),
Clock.SYSTEM,
Looper.myLooper(),
- TvSingletons.getSingletons(context).getRemoteConfig());
+ TvSingletons.getSingletons(context).getBackendKnobs(),
+ TvSingletons.getSingletons(context).getPerformanceMonitor(),
+ TvSingletons.getSingletons(context).getChannelDataManager());
}
@VisibleForTesting
ProgramDataManager(
+ Context context,
Executor executor,
ContentResolver contentResolver,
Clock time,
Looper looper,
- RemoteConfig remoteConfig) {
+ BackendKnobsFlags backendKnobsFlags,
+ PerformanceMonitor performanceMonitor,
+ ChannelDataManager channelDataManager) {
+ mContext = context;
mDbExecutor = executor;
mClock = time;
mContentResolver = contentResolver;
mHandler = new MyHandler(looper);
- mRemoteConfig = remoteConfig;
+ mBackendKnobsFlags = backendKnobsFlags;
+ mPerformanceMonitor = performanceMonitor;
+ mChannelDataManager = channelDataManager;
mProgramObserver =
new ContentObserver(mHandler) {
@Override
@@ -246,24 +259,43 @@ public class ProgramDataManager implements MemoryManageable {
}
}
- /** A listener interface to receive notification on program data retrieval from DB. */
- public interface Listener {
+ public void prefetchChannel(long channelId) {
+ if (mCompleteInfoChannelIds.add(channelId)) {
+ long startTimeMs =
+ Utils.floorTime(
+ mClock.currentTimeMillis() - PROGRAM_GUIDE_SNAP_TIME_MS,
+ PROGRAM_GUIDE_SNAP_TIME_MS);
+ long endTimeMs = startTimeMs + TimeUnit.HOURS.toMillis(getFetchDuration());
+ new SingleChannelPrefetchTask(channelId, startTimeMs, endTimeMs).executeOnDbThread();
+ }
+ }
+
+ /** A Callback interface to receive notification on program data retrieval from DB. */
+ public interface Callback {
/**
* Called when a Program data is now available through getProgram() after the DB operation
* is done which wasn't before. This would be called only if fetched data is around the
* selected program.
*/
void onProgramUpdated();
+
+ /**
+ * Called when we update complete program data of specific channel during scrolling. Data is
+ * loaded from DB on request basis.
+ *
+ * @param channelId
+ */
+ void onSingleChannelUpdated(long channelId);
}
- /** Adds the {@link Listener}. */
- public void addListener(Listener listener) {
- mListeners.add(listener);
+ /** Adds the {@link Callback}. */
+ public void addCallback(Callback callback) {
+ mCallbacks.add(callback);
}
- /** Removes the {@link Listener}. */
- public void removeListener(Listener listener) {
- mListeners.remove(listener);
+ /** Removes the {@link Callback}. */
+ public void removeCallback(Callback callback) {
+ mCallbacks.remove(callback);
}
/** Enables or Disables program prefetch. */
@@ -451,7 +483,7 @@ public class ProgramDataManager implements MemoryManageable {
}
clearTask(mProgramUpdateTaskMap);
mHandler.removeMessages(MSG_UPDATE_ONE_CURRENT_PROGRAM);
- mProgramsUpdateTask = new ProgramsUpdateTask(mContentResolver, mClock.currentTimeMillis());
+ mProgramsUpdateTask = new ProgramsUpdateTask(mClock.currentTimeMillis());
mProgramsUpdateTask.executeOnDbThread();
}
@@ -461,20 +493,29 @@ public class ProgramDataManager implements MemoryManageable {
private final long mEndTimeMs;
private boolean mSuccess;
+ private TimerEvent mFromEmptyCacheTimeEvent;
public ProgramsPrefetchTask() {
super(mDbExecutor);
long time = mClock.currentTimeMillis();
mStartTimeMs =
Utils.floorTime(time - PROGRAM_GUIDE_SNAP_TIME_MS, PROGRAM_GUIDE_SNAP_TIME_MS);
- mEndTimeMs =
- mStartTimeMs
- + TimeUnit.HOURS.toMillis(PROGRAM_GUIDE_MAX_HOURS.get(mRemoteConfig));
+ mEndTimeMs = mStartTimeMs + TimeUnit.HOURS.toMillis(getFetchDuration());
mSuccess = false;
}
@Override
+ protected void onPreExecute() {
+ if (mChannelIdCurrentProgramMap.isEmpty()) {
+ // No current program guide is shown.
+ // Measure the delay before users can see program guides.
+ mFromEmptyCacheTimeEvent = mPerformanceMonitor.startTimer();
+ }
+ }
+
+ @Override
protected Map<Long, ArrayList<Program>> doInBackground(Void... params) {
+ TimerEvent asyncTimeEvent = mPerformanceMonitor.startTimer();
Map<Long, ArrayList<Program>> programMap = new HashMap<>();
if (DEBUG) {
Log.d(
@@ -497,8 +538,19 @@ public class ProgramDataManager implements MemoryManageable {
return null;
}
programMap.clear();
- try (Cursor c =
- mContentResolver.query(uri, Program.PROJECTION, null, null, SORT_BY_TIME)) {
+
+ String[] projection =
+ mBackendKnobsFlags.enablePartialProgramFetch()
+ ? Program.PARTIAL_PROJECTION
+ : Program.PROJECTION;
+ if (TvProviderUtils.checkSeriesIdColumn(mContext, Programs.CONTENT_URI)) {
+ if (Utils.isProgramsUri(uri)) {
+ projection =
+ TvProviderUtils.addExtraColumnsToProjection(
+ projection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
+ }
+ }
+ try (Cursor c = mContentResolver.query(uri, projection, null, null, SORT_BY_TIME)) {
if (c == null) {
continue;
}
@@ -510,7 +562,10 @@ public class ProgramDataManager implements MemoryManageable {
}
return null;
}
- Program program = Program.fromCursor(c);
+ Program program =
+ mBackendKnobsFlags.enablePartialProgramFetch()
+ ? Program.fromCursorPartialProjection(c)
+ : Program.fromCursor(c);
if (Program.isDuplicate(program, lastReadProgram)) {
duplicateCount++;
continue;
@@ -520,6 +575,15 @@ public class ProgramDataManager implements MemoryManageable {
ArrayList<Program> programs = programMap.get(program.getChannelId());
if (programs == null) {
programs = new ArrayList<>();
+ if (mBackendKnobsFlags.enablePartialProgramFetch()) {
+ // To skip already loaded complete data.
+ Program currentProgramInfo =
+ mChannelIdCurrentProgramMap.get(program.getChannelId());
+ if (currentProgramInfo != null
+ && Program.isDuplicate(program, currentProgramInfo)) {
+ program = currentProgramInfo;
+ }
+ }
programMap.put(program.getChannelId(), programs);
}
programs.add(program);
@@ -534,12 +598,17 @@ public class ProgramDataManager implements MemoryManageable {
Log.d(TAG, "Database is changed while querying. Will retry.");
}
} catch (SecurityException e) {
- Log.d(TAG, "Security exception during program data query", e);
+ Log.w(TAG, "Security exception during program data query", e);
+ } catch (Exception e) {
+ Log.w(TAG, "Error during program data query", e);
}
}
if (DEBUG) {
Log.d(TAG, "Ends programs prefetch for " + programMap.size() + " channels");
}
+ mPerformanceMonitor.stopTimer(
+ asyncTimeEvent,
+ EventNames.PROGRAM_DATA_MANAGER_PROGRAMS_PREFETCH_TASK_DO_IN_BACKGROUND);
return programMap;
}
@@ -552,8 +621,6 @@ public class ProgramDataManager implements MemoryManageable {
}
long nextMessageDelayedTime;
if (mSuccess) {
- mChannelIdProgramCache = programs;
- notifyProgramUpdated();
long currentTime = mClock.currentTimeMillis();
mLastPrefetchTaskRunMs = currentTime;
nextMessageDelayedTime =
@@ -561,6 +628,22 @@ public class ProgramDataManager implements MemoryManageable {
mLastPrefetchTaskRunMs + PROGRAM_GUIDE_SNAP_TIME_MS,
PROGRAM_GUIDE_SNAP_TIME_MS)
- currentTime;
+ // Issue second pre-fetch immediately after the first partial update
+ if (mChannelIdProgramCache.isEmpty()) {
+ nextMessageDelayedTime = 0;
+ }
+ mChannelIdProgramCache = programs;
+ if (mBackendKnobsFlags.enablePartialProgramFetch()) {
+ // Since cache has partial data we need to reset the map of complete data.
+ mCompleteInfoChannelIds.clear();
+ }
+ notifyProgramUpdated();
+ if (mFromEmptyCacheTimeEvent != null) {
+ mPerformanceMonitor.stopTimer(
+ mFromEmptyCacheTimeEvent,
+ EventNames.PROGRAM_GUIDE_SHOW_FROM_EMPTY_CACHE);
+ mFromEmptyCacheTimeEvent = null;
+ }
} else {
nextMessageDelayedTime = PERIODIC_PROGRAM_UPDATE_MIN_MS;
}
@@ -571,17 +654,78 @@ public class ProgramDataManager implements MemoryManageable {
}
}
+ private long getFetchDuration() {
+ if (mChannelIdProgramCache.isEmpty()) {
+ return Math.max(1L, mBackendKnobsFlags.programGuideInitialFetchHours());
+ } else {
+ long durationHours;
+ int channelCount = mChannelDataManager.getChannelCount();
+ long knobsMaxHours = mBackendKnobsFlags.programGuideMaxHours();
+ long targetChannelCount = mBackendKnobsFlags.epgTargetChannelCount();
+ if (channelCount <= targetChannelCount) {
+ durationHours = Math.max(48L, knobsMaxHours);
+ } else {
+ // 2 days <= duration <= 14 days (336 hours)
+ durationHours = knobsMaxHours * targetChannelCount / channelCount;
+ if (durationHours < 48L) {
+ durationHours = 48L;
+ } else if (durationHours > 336L) {
+ durationHours = 336L;
+ }
+ }
+ return durationHours;
+ }
+ }
+
+ private class SingleChannelPrefetchTask extends AsyncDbTask.AsyncQueryTask<ArrayList<Program>> {
+ long mChannelId;
+
+ public SingleChannelPrefetchTask(long channelId, long startTimeMs, long endTimeMs) {
+ super(
+ mDbExecutor,
+ mContext,
+ TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs),
+ Program.PROJECTION,
+ null,
+ null,
+ SORT_BY_TIME);
+ mChannelId = channelId;
+ }
+
+ @Override
+ protected ArrayList<Program> onQuery(Cursor c) {
+ ArrayList<Program> programMap = new ArrayList<>();
+ while (c.moveToNext()) {
+ Program program = Program.fromCursor(c);
+ programMap.add(program);
+ }
+ return programMap;
+ }
+
+ @Override
+ protected void onPostExecute(ArrayList<Program> programs) {
+ mChannelIdProgramCache.put(mChannelId, programs);
+ notifySingleChannelUpdated(mChannelId);
+ }
+ }
+
private void notifyProgramUpdated() {
- for (Listener listener : mListeners) {
- listener.onProgramUpdated();
+ for (Callback callback : mCallbacks) {
+ callback.onProgramUpdated();
+ }
+ }
+
+ private void notifySingleChannelUpdated(long channelId) {
+ for (Callback callback : mCallbacks) {
+ callback.onSingleChannelUpdated(channelId);
}
}
private class ProgramsUpdateTask extends AsyncDbTask.AsyncQueryTask<List<Program>> {
- public ProgramsUpdateTask(ContentResolver contentResolver, long time) {
+ public ProgramsUpdateTask(long time) {
super(
mDbExecutor,
- contentResolver,
+ mContext,
Programs.CONTENT_URI
.buildUpon()
.appendQueryParameter(PARAM_START_TIME, String.valueOf(time))
@@ -633,6 +777,9 @@ public class ProgramDataManager implements MemoryManageable {
for (Long channelId : removedChannelIds) {
if (mPrefetchEnabled) {
mChannelIdProgramCache.remove(channelId);
+ if (mBackendKnobsFlags.enablePartialProgramFetch()) {
+ mCompleteInfoChannelIds.remove(channelId);
+ }
}
mChannelIdCurrentProgramMap.remove(channelId);
notifyCurrentProgramUpdate(channelId, null);
@@ -645,11 +792,10 @@ public class ProgramDataManager implements MemoryManageable {
private class UpdateCurrentProgramForChannelTask extends AsyncDbTask.AsyncQueryTask<Program> {
private final long mChannelId;
- private UpdateCurrentProgramForChannelTask(
- ContentResolver contentResolver, long channelId, long time) {
+ private UpdateCurrentProgramForChannelTask(long channelId, long time) {
super(
mDbExecutor,
- contentResolver,
+ mContext,
TvContract.buildProgramsUriForChannel(channelId, time, time),
Program.PROJECTION,
null,
@@ -695,7 +841,7 @@ public class ProgramDataManager implements MemoryManageable {
}
UpdateCurrentProgramForChannelTask task =
new UpdateCurrentProgramForChannelTask(
- mContentResolver, channelId, mClock.currentTimeMillis());
+ channelId, mClock.currentTimeMillis());
mProgramUpdateTaskMap.put(channelId, task);
task.executeOnDbThread();
break;
diff --git a/src/com/android/tv/data/WatchedHistoryManager.java b/src/com/android/tv/data/WatchedHistoryManager.java
index 7187efd1..9c1d423f 100644
--- a/src/com/android/tv/data/WatchedHistoryManager.java
+++ b/src/com/android/tv/data/WatchedHistoryManager.java
@@ -34,6 +34,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Scanner;
+import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
/**
@@ -73,24 +74,20 @@ public class WatchedHistoryManager {
// onNewRecordAdded will be called in the same thread as the thread
// which created this instance.
mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- for (long i = mLastIndex + 1; i <= lastIndex; ++i) {
- WatchedRecord record =
- decode(
- mSharedPreferences.getString(
- getSharedPreferencesKey(i),
- null));
- if (record != null) {
- mWatchedHistory.add(record);
- if (mListener != null) {
- mListener.onNewRecordAdded(record);
- }
+ () -> {
+ for (long i = mLastIndex + 1; i <= lastIndex; ++i) {
+ WatchedRecord record =
+ decode(
+ mSharedPreferences.getString(
+ getSharedPreferencesKey(i), null));
+ if (record != null) {
+ mWatchedHistory.add(record);
+ if (mListener != null) {
+ mListener.onNewRecordAdded(record);
}
}
- mLastIndex = lastIndex;
}
+ mLastIndex = lastIndex;
});
}
}
@@ -100,16 +97,18 @@ public class WatchedHistoryManager {
private Listener mListener;
private final int mMaxHistorySize;
private final Handler mHandler;
+ private final Executor mExecutor;
public WatchedHistoryManager(Context context) {
- this(context, MAX_HISTORY_SIZE);
+ this(context, MAX_HISTORY_SIZE, AsyncTask.THREAD_POOL_EXECUTOR);
}
@VisibleForTesting
- WatchedHistoryManager(Context context, int maxHistorySize) {
+ WatchedHistoryManager(Context context, int maxHistorySize, Executor executor) {
mContext = context.getApplicationContext();
mMaxHistorySize = maxHistorySize;
mHandler = new Handler();
+ mExecutor = executor;
}
/** Starts the manager. It loads history data from {@link SharedPreferences}. */
@@ -130,7 +129,7 @@ public class WatchedHistoryManager {
protected void onPostExecute(Void params) {
onLoadFinished();
}
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }.executeOnExecutor(mExecutor);
} else {
loadWatchedHistory();
onLoadFinished();
diff --git a/src/com/android/tv/data/api/Channel.java b/src/com/android/tv/data/api/Channel.java
index 496331cf..fb00952c 100644
--- a/src/com/android/tv/data/api/Channel.java
+++ b/src/com/android/tv/data/api/Channel.java
@@ -85,6 +85,8 @@ public interface Channel {
String getAppLinkIntentUri();
+ String getNetworkAffiliation();
+
String getLogoUri();
boolean isRecordingProhibited();
@@ -109,6 +111,8 @@ public interface Channel {
void setLogoUri(String logoUri);
+ void setNetworkAffiliation(String networkAffiliation);
+
boolean channelLogoExists();
void loadBitmap(
diff --git a/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java b/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java
deleted file mode 100644
index 795ad5c4..00000000
--- a/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2018 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 com.android.tv.data.api.Channel;
-
-/**
- * Hand copy of generated Autovalue class.
- *
- * TODO get autovalue working
- */
-final class AutoValue_EpgReader_EpgChannel extends EpgReader.EpgChannel {
-
- private final Channel channel;
- private final String epgChannelId;
-
- AutoValue_EpgReader_EpgChannel(
- Channel channel,
- String epgChannelId) {
- if (channel == null) {
- throw new NullPointerException("Null channel");
- }
- this.channel = channel;
- if (epgChannelId == null) {
- throw new NullPointerException("Null epgChannelId");
- }
- this.epgChannelId = epgChannelId;
- }
-
- @Override
- public Channel getChannel() {
- return channel;
- }
-
- @Override
- public String getEpgChannelId() {
- return epgChannelId;
- }
-
- @Override
- public String toString() {
- return "EpgChannel{"
- + "channel=" + channel + ", "
- + "epgChannelId=" + epgChannelId
- + "}";
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof EpgReader.EpgChannel) {
- EpgReader.EpgChannel that = (EpgReader.EpgChannel) o;
- return (this.channel.equals(that.getChannel()))
- && (this.epgChannelId.equals(that.getEpgChannelId()));
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h = 1;
- h *= 1000003;
- h ^= this.channel.hashCode();
- h *= 1000003;
- h ^= this.epgChannelId.hashCode();
- return h;
- }
-
-}
-
diff --git a/src/com/android/tv/data/epg/EpgFetchHelper.java b/src/com/android/tv/data/epg/EpgFetchHelper.java
index 3c7112ec..3843ca99 100644
--- a/src/com/android/tv/data/epg/EpgFetchHelper.java
+++ b/src/com/android/tv/data/epg/EpgFetchHelper.java
@@ -17,6 +17,7 @@
package com.android.tv.data.epg;
import android.content.ContentProviderOperation;
+import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
@@ -30,9 +31,13 @@ import android.util.Log;
import com.android.tv.common.CommonConstants;
import com.android.tv.common.util.Clock;
import com.android.tv.data.Program;
+import com.android.tv.data.api.Channel;
+import com.android.tv.features.TvFeatures;
+import com.android.tv.util.TvProviderUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
/** The helper class for {@link EpgFetcher} */
@@ -101,7 +106,7 @@ class EpgFetchHelper {
ops.add(
ContentProviderOperation.newUpdate(
TvContract.buildProgramUri(oldProgram.getId()))
- .withValues(Program.toContentValues(newProgram))
+ .withValues(Program.toContentValues(newProgram, context))
.build());
oldProgramsIndex++;
newProgramsIndex++;
@@ -127,7 +132,7 @@ class EpgFetchHelper {
if (addNewProgram) {
ops.add(
ContentProviderOperation.newInsert(Programs.CONTENT_URI)
- .withValues(Program.toContentValues(newProgram))
+ .withValues(Program.toContentValues(newProgram, context))
.build());
}
// Throttle the batch operation not to cause TransactionTooLargeException.
@@ -155,14 +160,57 @@ class EpgFetchHelper {
return updated;
}
+ @WorkerThread
+ static void updateNetworkAffiliation(Context context, Set<EpgReader.EpgChannel> channels) {
+ if (!TvFeatures.STORE_NETWORK_AFFILIATION.isEnabled(context)) {
+ return;
+ }
+ ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ for (EpgReader.EpgChannel epgChannel : channels) {
+ if (!epgChannel.getDbUpdateNeeded()) {
+ continue;
+ }
+ Channel channel = epgChannel.getChannel();
+
+ ContentValues values = new ContentValues();
+ values.put(
+ TvContract.Channels.COLUMN_NETWORK_AFFILIATION,
+ channel.getNetworkAffiliation());
+ ops.add(
+ ContentProviderOperation.newUpdate(TvContract.buildChannelUri(channel.getId()))
+ .withValues(values)
+ .build());
+ if (ops.size() >= BATCH_OPERATION_COUNT) {
+ try {
+ context.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.e(TAG, "Failed to update channels.", e);
+ }
+ ops.clear();
+ }
+ }
+ try {
+ context.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.e(TAG, "Failed to update channels.", e);
+ }
+ }
+
+ @WorkerThread
private static List<Program> queryPrograms(
Context context, long channelId, long startTimeMs, long endTimeMs) {
+ String[] projection = Program.PROJECTION;
+ if (TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) {
+ projection =
+ TvProviderUtils.addExtraColumnsToProjection(
+ projection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
+ }
try (Cursor c =
context.getContentResolver()
.query(
TvContract.buildProgramsUriForChannel(
channelId, startTimeMs, endTimeMs),
- Program.PROJECTION,
+ projection,
null,
null,
Programs.COLUMN_START_TIME_UTC_MILLIS)) {
diff --git a/src/com/android/tv/data/epg/EpgFetcherImpl.java b/src/com/android/tv/data/epg/EpgFetcherImpl.java
index 2aaaa5b2..b191421f 100644
--- a/src/com/android/tv/data/epg/EpgFetcherImpl.java
+++ b/src/com/android/tv/data/epg/EpgFetcherImpl.java
@@ -38,11 +38,10 @@ import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.TvFeatures;
import com.android.tv.TvSingletons;
import com.android.tv.common.BuildConfig;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.config.api.RemoteConfigValue;
+import com.android.tv.common.buildtype.HasBuildType;
import com.android.tv.common.util.Clock;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.common.util.LocationUtils;
@@ -55,12 +54,15 @@ import com.android.tv.data.ChannelLogoFetcher;
import com.android.tv.data.Lineup;
import com.android.tv.data.Program;
import com.android.tv.data.api.Channel;
+import com.android.tv.features.TvFeatures;
import com.android.tv.perf.EventNames;
import com.android.tv.perf.PerformanceMonitor;
import com.android.tv.perf.TimerEvent;
import com.android.tv.util.Utils;
import com.google.android.tv.partner.support.EpgInput;
import com.google.android.tv.partner.support.EpgInputs;
+import com.google.common.collect.ImmutableSet;
+import com.android.tv.common.flags.BackendKnobsFlags;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
@@ -100,8 +102,7 @@ public class EpgFetcherImpl implements EpgFetcher {
private static final long FETCH_DURING_SCAN_DURATION_SEC = TimeUnit.HOURS.toSeconds(3);
private static final long FAST_FETCH_DURATION_SEC = TimeUnit.DAYS.toSeconds(2);
- private static final RemoteConfigValue<Long> ROUTINE_INTERVAL_HOUR =
- RemoteConfigValue.create("live_channels_epg_fetcher_interval_hour", 4);
+ private static final long DEFAULT_ROUTINE_INTERVAL_HOUR = 4;
private static final int MSG_PREPARE_FETCH_DURING_SCAN = 1;
private static final int MSG_CHANNEL_UPDATED_DURING_SCAN = 2;
@@ -115,6 +116,9 @@ public class EpgFetcherImpl implements EpgFetcher {
private final ChannelDataManager mChannelDataManager;
private final EpgReader mEpgReader;
private final PerformanceMonitor mPerformanceMonitor;
+ private final EpgInputWhiteList mEpgInputWhiteList;
+ private final BackendKnobsFlags mBackendKnobsFlags;
+ private final HasBuildType.BuildType mBuildType;
private FetchAsyncTask mFetchTask;
private FetchDuringScanHandler mFetchDuringScanHandler;
private long mEpgTimeStamp;
@@ -124,9 +128,6 @@ public class EpgFetcherImpl implements EpgFetcher {
// A flag to block the re-entrance of onChannelScanStarted and onChannelScanFinished.
private boolean mScanStarted;
- private final long mRoutineIntervalMs;
- private final long mEpgDataExpiredTimeLimitMs;
- private final long mFastFetchDurationSec;
private Clock mClock;
public static EpgFetcher create(Context context) {
@@ -136,36 +137,54 @@ public class EpgFetcherImpl implements EpgFetcher {
PerformanceMonitor performanceMonitor = tvSingletons.getPerformanceMonitor();
EpgReader epgReader = tvSingletons.providesEpgReader().get();
Clock clock = tvSingletons.getClock();
- long routineIntervalMs = ROUTINE_INTERVAL_HOUR.get(tvSingletons.getRemoteConfig());
-
+ EpgInputWhiteList epgInputWhiteList =
+ new EpgInputWhiteList(tvSingletons.getCloudEpgFlags());
+ BackendKnobsFlags backendKnobsFlags = tvSingletons.getBackendKnobs();
+ HasBuildType.BuildType buildType = tvSingletons.getBuildType();
return new EpgFetcherImpl(
context,
+ epgInputWhiteList,
channelDataManager,
epgReader,
performanceMonitor,
clock,
- routineIntervalMs);
+ backendKnobsFlags,
+ buildType);
}
@VisibleForTesting
EpgFetcherImpl(
Context context,
+ EpgInputWhiteList epgInputWhiteList,
ChannelDataManager channelDataManager,
EpgReader epgReader,
PerformanceMonitor performanceMonitor,
Clock clock,
- long routineIntervalMs) {
+ BackendKnobsFlags backendKnobsFlags,
+ HasBuildType.BuildType buildType) {
mContext = context;
mChannelDataManager = channelDataManager;
mEpgReader = epgReader;
mPerformanceMonitor = performanceMonitor;
mClock = clock;
- mRoutineIntervalMs =
- routineIntervalMs <= 0
- ? TimeUnit.HOURS.toMillis(ROUTINE_INTERVAL_HOUR.getDefaultValue())
- : TimeUnit.HOURS.toMillis(routineIntervalMs);
- mEpgDataExpiredTimeLimitMs = routineIntervalMs * 2;
- mFastFetchDurationSec = FAST_FETCH_DURATION_SEC + routineIntervalMs / 1000;
+ mEpgInputWhiteList = epgInputWhiteList;
+ mBackendKnobsFlags = backendKnobsFlags;
+ mBuildType = buildType;
+ }
+
+ private long getFastFetchDurationSec() {
+ return FAST_FETCH_DURATION_SEC + getRoutineIntervalMs() / 1000;
+ }
+
+ private long getEpgDataExpiredTimeLimitMs() {
+ return getRoutineIntervalMs() * 2;
+ }
+
+ private long getRoutineIntervalMs() {
+ long routineIntervalHours = mBackendKnobsFlags.epgFetcherIntervalHour();
+ return routineIntervalHours <= 0
+ ? TimeUnit.HOURS.toMillis(DEFAULT_ROUTINE_INTERVAL_HOUR)
+ : TimeUnit.HOURS.toMillis(routineIntervalHours);
}
private static Set<Channel> getExistingChannelsForMyPackage(Context context) {
@@ -214,7 +233,7 @@ public class EpgFetcherImpl implements EpgFetcher {
new JobInfo.Builder(
EPG_ROUTINELY_FETCHING_JOB_ID,
new ComponentName(mContext, EpgFetchService.class))
- .setPeriodic(mRoutineIntervalMs)
+ .setPeriodic(getRoutineIntervalMs())
.setBackoffCriteria(INITIAL_BACKOFF_MS, JobInfo.BACKOFF_POLICY_EXPONENTIAL)
.setPersisted(true)
.build();
@@ -238,7 +257,7 @@ public class EpgFetcherImpl implements EpgFetcher {
@Override
protected void onPostExecute(Long result) {
if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
- > mEpgDataExpiredTimeLimitMs) {
+ > getEpgDataExpiredTimeLimitMs()) {
Log.i(TAG, "EPG data expired. Start fetching immediately.");
fetchImmediately();
}
@@ -346,6 +365,19 @@ public class EpgFetcherImpl implements EpgFetcher {
if (DEBUG) Log.d(TAG, "Cannot start routine service: scanning channels.");
return false;
}
+ if (!TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.isEnabled(mContext)
+ && mBuildType != HasBuildType.BuildType.AOSP) {
+ if (getTunerChannelCount() == 0) {
+ if (DEBUG) Log.d(TAG, "Cannot start routine service: no internal tuner channels.");
+ return false;
+ }
+ if (!TextUtils.isEmpty(EpgFetchHelper.getLastLineupId(mContext))) {
+ return true;
+ }
+ if (!TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
+ return true;
+ }
+ }
return true;
}
@@ -505,6 +537,17 @@ public class EpgFetcherImpl implements EpgFetcher {
return numbers.size();
}
+ private boolean isInputInWhiteList(EpgInput epgInput) {
+ if (mBuildType == HasBuildType.BuildType.AOSP) {
+ return false;
+ }
+ return (BuildConfig.ENG
+ && epgInput.getInputId()
+ .equals(
+ "com.example.partnersupportsampletvinput/.SampleTvInputService"))
+ || mEpgInputWhiteList.isInputWhiteListed(epgInput.getInputId());
+ }
+
@VisibleForTesting
class FetchAsyncTask extends AsyncTask<Void, Void, Integer> {
private final JobService mService;
@@ -532,12 +575,45 @@ public class EpgFetcherImpl implements EpgFetcher {
Integer builtInResult = fetchEpgForBuiltInTuner();
boolean anyCloudEpgFailure = false;
boolean anyCloudEpgSuccess = false;
+ if (TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.isEnabled(mContext)
+ && mBuildType != HasBuildType.BuildType.AOSP) {
+ for (EpgInput epgInput : getEpgInputs()) {
+ if (DEBUG) Log.d(TAG, "Start EPG fetch for " + epgInput);
+ if (isCancelled()) {
+ break;
+ }
+ if (isInputInWhiteList(epgInput)) {
+ // TODO(b/66191312) check timestamp and result code and decide if update
+ // is needed.
+ Set<Channel> channels = getExistingChannelsFor(epgInput.getInputId());
+ Integer result = fetchEpgFor(epgInput.getLineupId(), channels);
+ anyCloudEpgFailure = anyCloudEpgFailure || result != null;
+ anyCloudEpgSuccess = anyCloudEpgSuccess || result == null;
+ updateCloudEpgInput(epgInput, result);
+ } else {
+ Log.w(
+ TAG,
+ "Fetching the EPG for "
+ + epgInput.getInputId()
+ + " is not supported.");
+ }
+ }
+ }
+ if (builtInResult == null || builtInResult == REASON_NO_BUILT_IN_CHANNELS) {
+ return anyCloudEpgFailure
+ ? ((Integer) REASON_CLOUD_EPG_FAILURE)
+ : anyCloudEpgSuccess ? null : builtInResult;
+ }
return builtInResult;
} finally {
TrafficStats.setThreadStatsTag(oldTag);
}
}
+ private void updateCloudEpgInput(EpgInput unusedEpgInput, Integer unusedResult) {
+ // TODO(b/66191312) write the result and timestamp to the input table
+ }
+
private Set<Channel> getExistingChannelsFor(String inputId) {
Set<Channel> result = new HashSet<>();
try (Cursor cursor =
@@ -548,13 +624,24 @@ public class EpgFetcherImpl implements EpgFetcher {
null,
null,
null)) {
- while (cursor.moveToNext()) {
- result.add(ChannelImpl.fromCursor(cursor));
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ result.add(ChannelImpl.fromCursor(cursor));
+ }
}
return result;
}
}
+ private Set<EpgInput> getEpgInputs() {
+ if (mBuildType == HasBuildType.BuildType.AOSP) {
+ return ImmutableSet.of();
+ }
+ Set<EpgInput> epgInputs = EpgInputs.queryEpgInputs(mContext.getContentResolver());
+ if (DEBUG) Log.d(TAG, "getEpgInputs " + epgInputs);
+ return epgInputs;
+ }
+
private Integer fetchEpgForBuiltInTuner() {
try {
Integer failureReason = prepareFetchEpg(false);
@@ -606,19 +693,16 @@ public class EpgFetcherImpl implements EpgFetcher {
Log.i(TAG, "Failed to get EPG channels for " + lineupId);
return REASON_NO_EPG_DATA_RETURNED;
}
+ EpgFetchHelper.updateNetworkAffiliation(mContext, channels);
if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
- > mEpgDataExpiredTimeLimitMs) {
- batchFetchEpg(channels, mFastFetchDurationSec);
+ > getEpgDataExpiredTimeLimitMs()) {
+ batchFetchEpg(channels, getFastFetchDurationSec());
}
new Handler(mContext.getMainLooper())
.post(
- new Runnable() {
- @Override
- public void run() {
+ () ->
ChannelLogoFetcher.startFetchingChannelLogos(
- mContext, asChannelList(channels));
- }
- });
+ mContext, asChannelList(channels)));
for (EpgReader.EpgChannel epgChannel : channels) {
if (this.isCancelled()) {
return null;
@@ -780,6 +864,9 @@ public class EpgFetcherImpl implements EpgFetcher {
mFetchedChannelIdsDuringScan.add(epgChannel.getChannel().getId());
}
}
+ if (!newChannels.isEmpty()) {
+ EpgFetchHelper.updateNetworkAffiliation(mContext, newChannels);
+ }
batchFetchEpg(newChannels, FETCH_DURING_SCAN_DURATION_SEC);
}
@@ -798,14 +885,7 @@ public class EpgFetcherImpl implements EpgFetcher {
// Clear timestamp to make routine service start right away.
EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, 0);
Log.i(TAG, "EPG Fetching during channel scanning finished.");
- new Handler(Looper.getMainLooper())
- .post(
- new Runnable() {
- @Override
- public void run() {
- fetchImmediately();
- }
- });
+ new Handler(Looper.getMainLooper()).post(EpgFetcherImpl.this::fetchImmediately);
}
}
}
diff --git a/src/com/android/tv/data/epg/EpgInputWhiteList.java b/src/com/android/tv/data/epg/EpgInputWhiteList.java
index eada8b24..24b4fe3d 100644
--- a/src/com/android/tv/data/epg/EpgInputWhiteList.java
+++ b/src/com/android/tv/data/epg/EpgInputWhiteList.java
@@ -21,8 +21,8 @@ import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import com.android.tv.common.BuildConfig;
-import com.android.tv.common.config.api.RemoteConfig;
import com.android.tv.common.experiments.Experiments;
+import com.android.tv.common.flags.CloudEpgFlags;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
@@ -33,7 +33,6 @@ import java.util.Set;
public final class EpgInputWhiteList {
private static final boolean DEBUG = false;
private static final String TAG = "EpgInputWhiteList";
- @VisibleForTesting public static final String KEY = "live_channels_3rd_party_epg_inputs";
private static final String QA_DEV_INPUTS =
"com.example.partnersupportsampletvinput/.SampleTvInputService,"
+ "com.android.tv.tuner.sample.dvb/.tvinput.SampleDvbTunerTvInputService";
@@ -44,10 +43,10 @@ public final class EpgInputWhiteList {
return inputId == null ? null : inputId.substring(0, inputId.indexOf("/"));
}
- private final RemoteConfig remoteConfig;
+ private final CloudEpgFlags cloudEpgFlags;
- public EpgInputWhiteList(RemoteConfig remoteConfig) {
- this.remoteConfig = remoteConfig;
+ public EpgInputWhiteList(CloudEpgFlags cloudEpgFlags) {
+ this.cloudEpgFlags = cloudEpgFlags;
}
public boolean isInputWhiteListed(String inputId) {
@@ -72,7 +71,7 @@ public final class EpgInputWhiteList {
}
private Set<String> getWhiteListedInputs() {
- Set<String> result = toInputSet(remoteConfig.getString(KEY));
+ Set<String> result = toInputSet(cloudEpgFlags.thirdPartyEpgInputsCsv());
if (BuildConfig.ENG || Experiments.ENABLE_QA_FEATURES.get()) {
HashSet<String> moreInputs = new HashSet<>(toInputSet(QA_DEV_INPUTS));
if (result.isEmpty()) {
diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java
index 7147905a..c9fcd979 100644
--- a/src/com/android/tv/data/epg/EpgReader.java
+++ b/src/com/android/tv/data/epg/EpgReader.java
@@ -23,6 +23,7 @@ import com.android.tv.data.Lineup;
import com.android.tv.data.Program;
import com.android.tv.data.api.Channel;
import com.android.tv.dvr.data.SeriesInfo;
+import com.google.auto.value.AutoValue;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -33,15 +34,18 @@ import java.util.Set;
public interface EpgReader {
/** Value class that holds a EpgChannelId and its corresponding {@link Channel} */
- // TODO(b/72052568): Get autovalue to work in aosp master
+ @AutoValue
abstract class EpgChannel {
- public static EpgChannel createEpgChannel(Channel channel, String epgChannelId) {
- return new AutoValue_EpgReader_EpgChannel(channel, epgChannelId);
+ public static EpgChannel createEpgChannel(Channel channel, String epgChannelId,
+ boolean dbUpdateNeeded) {
+ return new AutoValue_EpgReader_EpgChannel(channel, epgChannelId, dbUpdateNeeded);
}
public abstract Channel getChannel();
public abstract String getEpgChannelId();
+
+ public abstract boolean getDbUpdateNeeded();
}
/** Checks if the reader is available. */
diff --git a/src/com/android/tv/dialog/PinDialogFragment.java b/src/com/android/tv/dialog/PinDialogFragment.java
index 71f45fbe..87308093 100644
--- a/src/com/android/tv/dialog/PinDialogFragment.java
+++ b/src/com/android/tv/dialog/PinDialogFragment.java
@@ -16,37 +16,26 @@
package com.android.tv.dialog;
-import android.animation.Animator;
-import android.animation.AnimatorInflater;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
import android.app.ActivityManager;
import android.app.Dialog;
-import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
-import android.content.res.Resources;
import android.media.tv.TvContentRating;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.text.TextUtils;
-import android.util.AttributeSet;
import android.util.Log;
-import android.util.TypedValue;
-import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
-import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
+import com.android.tv.dialog.picker.PinPicker;
import com.android.tv.util.TvSettings;
public class PinDialogFragment extends SafeDismissDialogFragment {
@@ -77,17 +66,12 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
private static final int MAX_WRONG_PIN_COUNT = 5;
private static final int DISABLE_PIN_DURATION_MILLIS = 60 * 1000; // 1 minute
- private static final String INITIAL_TEXT = "—";
private static final String TRACKER_LABEL = "Pin dialog";
private static final String ARGS_TYPE = "args_type";
private static final String ARGS_RATING = "args_rating";
public static final String DIALOG_TAG = PinDialogFragment.class.getName();
- private static final int NUMBER_PICKERS_RES_ID[] = {
- R.id.first, R.id.second, R.id.third, R.id.fourth
- };
-
private int mType;
private int mRequestType;
private boolean mPinChecked;
@@ -96,7 +80,7 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
private TextView mWrongPinView;
private View mEnterPinView;
private TextView mTitleView;
- private PinNumberPicker[] mPickers;
+ private PinPicker mPicker;
private SharedPreferences mSharedPreferences;
private String mPrevPin;
private String mPin;
@@ -140,7 +124,6 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dlg = super.onCreateDialog(savedInstanceState);
dlg.getWindow().getAttributes().windowAnimations = R.style.pin_dialog_animation;
- PinNumberPicker.loadResources(dlg.getContext());
return dlg;
}
@@ -171,6 +154,14 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
mWrongPinView = (TextView) v.findViewById(R.id.wrong_pin);
mEnterPinView = v.findViewById(R.id.enter_pin);
mTitleView = (TextView) mEnterPinView.findViewById(R.id.title);
+ mPicker = v.findViewById(R.id.pin_picker);
+ mPicker.setOnClickListener(
+ view -> {
+ String pin = getPinInput();
+ if (!TextUtils.isEmpty(pin)) {
+ done(pin);
+ }
+ });
if (TextUtils.isEmpty(getPin())) {
// If PIN isn't set, user should set a PIN.
// Successfully setting a new set is considered as entering correct PIN.
@@ -210,31 +201,13 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
}
}
- mPickers = new PinNumberPicker[NUMBER_PICKERS_RES_ID.length];
- for (int i = 0; i < NUMBER_PICKERS_RES_ID.length; i++) {
- mPickers[i] = (PinNumberPicker) v.findViewById(NUMBER_PICKERS_RES_ID[i]);
- mPickers[i].setValueRangeAndResetText(0, 9);
- mPickers[i].setPinDialogFragment(this);
- mPickers[i].updateFocus(false);
- }
- for (int i = 0; i < NUMBER_PICKERS_RES_ID.length - 1; i++) {
- mPickers[i].setNextNumberPicker(mPickers[i + 1]);
- }
-
if (mType != PIN_DIALOG_TYPE_NEW_PIN) {
updateWrongPin();
}
+ mPicker.requestFocus();
return v;
}
- private final Runnable mUpdateEnterPinRunnable =
- new Runnable() {
- @Override
- public void run() {
- updateWrongPin();
- }
- };
-
private void updateWrongPin() {
if (getActivity() == null) {
// The activity is already detached. No need to update.
@@ -257,7 +230,8 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
R.plurals.pin_enter_countdown,
remainingSeconds,
remainingSeconds));
- mHandler.postDelayed(mUpdateEnterPinRunnable, 1000);
+
+ mHandler.postDelayed(this::updateWrongPin, 1000);
}
}
@@ -364,383 +338,11 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
}
private String getPinInput() {
- String result = "";
- try {
- for (PinNumberPicker pnp : mPickers) {
- pnp.updateText();
- result += pnp.getValue();
- }
- } catch (IllegalStateException e) {
- result = "";
- }
- return result;
+ return mPicker.getPinInput();
}
private void resetPinInput() {
- for (PinNumberPicker pnp : mPickers) {
- pnp.setValueRangeAndResetText(0, 9);
- }
- mPickers[0].requestFocus();
- }
-
- public static class PinNumberPicker extends FrameLayout {
- private static final int NUMBER_VIEWS_RES_ID[] = {
- R.id.previous2_number,
- R.id.previous_number,
- R.id.current_number,
- R.id.next_number,
- R.id.next2_number
- };
- private static final int CURRENT_NUMBER_VIEW_INDEX = 2;
- private static final int NOT_INITIALIZED = Integer.MIN_VALUE;
-
- private static Animator sFocusedNumberEnterAnimator;
- private static Animator sFocusedNumberExitAnimator;
- private static Animator sAdjacentNumberEnterAnimator;
- private static Animator sAdjacentNumberExitAnimator;
-
- private static float sAlphaForFocusedNumber;
- private static float sAlphaForAdjacentNumber;
-
- private int mMinValue;
- private int mMaxValue;
- private int mCurrentValue;
- // a value for setting mCurrentValue at the end of scroll animation.
- private int mNextValue;
- private final int mNumberViewHeight;
- private PinDialogFragment mDialog;
- private PinNumberPicker mNextNumberPicker;
- private boolean mCancelAnimation;
-
- private final View mNumberViewHolder;
- // When the PinNumberPicker has focus, mBackgroundView will show the focused background.
- // Also, this view is used for handling the text change animation of the current number
- // view which is required when the current number view text is changing from INITIAL_TEXT
- // to "0".
- private final TextView mBackgroundView;
- private final TextView[] mNumberViews;
- private final AnimatorSet mFocusGainAnimator;
- private final AnimatorSet mFocusLossAnimator;
- private final AnimatorSet mScrollAnimatorSet;
-
- public PinNumberPicker(Context context) {
- this(context, null);
- }
-
- public PinNumberPicker(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public PinNumberPicker(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public PinNumberPicker(
- Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- View view = inflate(context, R.layout.pin_number_picker, this);
- mNumberViewHolder = view.findViewById(R.id.number_view_holder);
- mBackgroundView = (TextView) view.findViewById(R.id.focused_background);
- mNumberViews = new TextView[NUMBER_VIEWS_RES_ID.length];
- for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
- mNumberViews[i] = (TextView) view.findViewById(NUMBER_VIEWS_RES_ID[i]);
- }
- Resources resources = context.getResources();
- mNumberViewHeight =
- resources.getDimensionPixelSize(R.dimen.pin_number_picker_text_view_height);
-
- mNumberViewHolder.setOnFocusChangeListener(
- new OnFocusChangeListener() {
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- updateFocus(true);
- }
- });
-
- mNumberViewHolder.setOnKeyListener(
- new OnKeyListener() {
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (event.getAction() == KeyEvent.ACTION_DOWN) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_UP:
- case KeyEvent.KEYCODE_DPAD_DOWN:
- {
- if (mCancelAnimation) {
- mScrollAnimatorSet.end();
- }
- if (!mScrollAnimatorSet.isRunning()) {
- mCancelAnimation = false;
- if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
- mNextValue =
- adjustValueInValidRange(
- mCurrentValue + 1);
- startScrollAnimation(true);
- } else {
- mNextValue =
- adjustValueInValidRange(
- mCurrentValue - 1);
- startScrollAnimation(false);
- }
- }
- return true;
- }
- }
- } else if (event.getAction() == KeyEvent.ACTION_UP) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_UP:
- case KeyEvent.KEYCODE_DPAD_DOWN:
- {
- mCancelAnimation = true;
- return true;
- }
- }
- }
- return false;
- }
- });
- mNumberViewHolder.setScrollY(mNumberViewHeight);
-
- mFocusGainAnimator = new AnimatorSet();
- mFocusGainAnimator.playTogether(
- ObjectAnimator.ofFloat(
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1],
- "alpha",
- 0f,
- sAlphaForAdjacentNumber),
- ObjectAnimator.ofFloat(
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX],
- "alpha",
- sAlphaForFocusedNumber,
- 0f),
- ObjectAnimator.ofFloat(
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1],
- "alpha",
- 0f,
- sAlphaForAdjacentNumber),
- ObjectAnimator.ofFloat(mBackgroundView, "alpha", 0f, 1f));
- mFocusGainAnimator.setDuration(
- context.getResources().getInteger(android.R.integer.config_shortAnimTime));
- mFocusGainAnimator.addListener(
- new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setText(
- mBackgroundView.getText());
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha(
- sAlphaForFocusedNumber);
- mBackgroundView.setText("");
- }
- });
-
- mFocusLossAnimator = new AnimatorSet();
- mFocusLossAnimator.playTogether(
- ObjectAnimator.ofFloat(
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1],
- "alpha",
- sAlphaForAdjacentNumber,
- 0f),
- ObjectAnimator.ofFloat(
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1],
- "alpha",
- sAlphaForAdjacentNumber,
- 0f),
- ObjectAnimator.ofFloat(mBackgroundView, "alpha", 1f, 0f));
- mFocusLossAnimator.setDuration(
- context.getResources().getInteger(android.R.integer.config_shortAnimTime));
-
- mScrollAnimatorSet = new AnimatorSet();
- mScrollAnimatorSet.setDuration(
- context.getResources().getInteger(R.integer.pin_number_scroll_duration));
- mScrollAnimatorSet.addListener(
- new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- // Set mCurrent value when scroll animation is finished.
- mCurrentValue = mNextValue;
- updateText();
- mNumberViewHolder.setScrollY(mNumberViewHeight);
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(
- sAlphaForAdjacentNumber);
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha(
- sAlphaForFocusedNumber);
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(
- sAlphaForAdjacentNumber);
- }
- });
- }
-
- static void loadResources(Context context) {
- if (sFocusedNumberEnterAnimator == null) {
- TypedValue outValue = new TypedValue();
- context.getResources()
- .getValue(R.dimen.pin_alpha_for_focused_number, outValue, true);
- sAlphaForFocusedNumber = outValue.getFloat();
- context.getResources()
- .getValue(R.dimen.pin_alpha_for_adjacent_number, outValue, true);
- sAlphaForAdjacentNumber = outValue.getFloat();
-
- sFocusedNumberEnterAnimator =
- AnimatorInflater.loadAnimator(context, R.animator.pin_focused_number_enter);
- sFocusedNumberExitAnimator =
- AnimatorInflater.loadAnimator(context, R.animator.pin_focused_number_exit);
- sAdjacentNumberEnterAnimator =
- AnimatorInflater.loadAnimator(
- context, R.animator.pin_adjacent_number_enter);
- sAdjacentNumberExitAnimator =
- AnimatorInflater.loadAnimator(context, R.animator.pin_adjacent_number_exit);
- }
- }
-
- @Override
- public boolean dispatchKeyEvent(KeyEvent event) {
- if (event.getAction() == KeyEvent.ACTION_UP) {
- int keyCode = event.getKeyCode();
- if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
- mNextValue = adjustValueInValidRange(keyCode - KeyEvent.KEYCODE_0);
- updateFocus(false);
- } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER
- || keyCode == KeyEvent.KEYCODE_ENTER) {
- if (mNextNumberPicker == null) {
- String pin = mDialog.getPinInput();
- if (!TextUtils.isEmpty(pin)) {
- mDialog.done(pin);
- }
- } else {
- mNextNumberPicker.requestFocus();
- }
- return true;
- }
- }
- return super.dispatchKeyEvent(event);
- }
-
- void startScrollAnimation(boolean scrollUp) {
- mFocusGainAnimator.end();
- mFocusLossAnimator.end();
- final ValueAnimator scrollAnimator =
- ValueAnimator.ofInt(0, scrollUp ? mNumberViewHeight : -mNumberViewHeight);
- scrollAnimator.addUpdateListener(
- new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- int value = (Integer) animation.getAnimatedValue();
- mNumberViewHolder.setScrollY(value + mNumberViewHeight);
- }
- });
- scrollAnimator.setDuration(
- getResources().getInteger(R.integer.pin_number_scroll_duration));
-
- if (scrollUp) {
- sAdjacentNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1]);
- sFocusedNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX]);
- sFocusedNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1]);
- sAdjacentNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 2]);
- } else {
- sAdjacentNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 2]);
- sFocusedNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1]);
- sFocusedNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX]);
- sAdjacentNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1]);
- }
-
- mScrollAnimatorSet.playTogether(
- scrollAnimator,
- sAdjacentNumberExitAnimator,
- sFocusedNumberExitAnimator,
- sFocusedNumberEnterAnimator,
- sAdjacentNumberEnterAnimator);
- mScrollAnimatorSet.start();
- }
-
- void setValueRangeAndResetText(int min, int max) {
- if (min > max) {
- throw new IllegalArgumentException(
- "The min value should be greater than or equal to the max value");
- } else if (min == NOT_INITIALIZED) {
- throw new IllegalArgumentException(
- "The min value should be greater than Integer.MIN_VALUE.");
- }
- mMinValue = min;
- mMaxValue = max;
- mNextValue = mCurrentValue = NOT_INITIALIZED;
- for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
- mNumberViews[i].setText(i == CURRENT_NUMBER_VIEW_INDEX ? INITIAL_TEXT : "");
- }
- mBackgroundView.setText(INITIAL_TEXT);
- }
-
- void setPinDialogFragment(PinDialogFragment dlg) {
- mDialog = dlg;
- }
-
- void setNextNumberPicker(PinNumberPicker picker) {
- mNextNumberPicker = picker;
- }
-
- int getValue() {
- if (mCurrentValue < mMinValue || mCurrentValue > mMaxValue) {
- throw new IllegalStateException("Value is not set");
- }
- return mCurrentValue;
- }
-
- void updateFocus(boolean withAnimation) {
- mScrollAnimatorSet.end();
- mFocusGainAnimator.end();
- mFocusLossAnimator.end();
- updateText();
- if (mNumberViewHolder.isFocused()) {
- if (withAnimation) {
- mBackgroundView.setText(String.valueOf(mCurrentValue));
- mFocusGainAnimator.start();
- } else {
- mBackgroundView.setAlpha(1f);
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(sAlphaForAdjacentNumber);
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(sAlphaForAdjacentNumber);
- }
- } else {
- if (withAnimation) {
- mFocusLossAnimator.start();
- } else {
- mBackgroundView.setAlpha(0f);
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(0f);
- mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(0f);
- }
- mNumberViewHolder.setScrollY(mNumberViewHeight);
- }
- }
-
- private void updateText() {
- boolean wasNotInitialized = false;
- if (mNumberViewHolder.isFocused() && mCurrentValue == NOT_INITIALIZED) {
- mNextValue = mCurrentValue = mMinValue;
- wasNotInitialized = true;
- }
- if (mCurrentValue >= mMinValue && mCurrentValue <= mMaxValue) {
- for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
- if (wasNotInitialized && i == CURRENT_NUMBER_VIEW_INDEX) {
- // In order to show the text change animation, keep the text of
- // mNumberViews[CURRENT_NUMBER_VIEW_INDEX].
- } else {
- mNumberViews[i].setText(
- String.valueOf(
- adjustValueInValidRange(
- mCurrentValue - CURRENT_NUMBER_VIEW_INDEX + i)));
- }
- }
- }
- }
-
- private int adjustValueInValidRange(int value) {
- int interval = mMaxValue - mMinValue + 1;
- if (value < mMinValue - interval || value > mMaxValue + interval) {
- throw new IllegalArgumentException(
- "The value( " + value + ") is too small or too big to adjust");
- }
- return (value < mMinValue)
- ? value + interval
- : (value > mMaxValue) ? value - interval : value;
- }
+ mPicker.resetPinInput();
}
/**
diff --git a/src/com/android/tv/dialog/picker/PinPicker.java b/src/com/android/tv/dialog/picker/PinPicker.java
new file mode 100644
index 00000000..f501dfd1
--- /dev/null
+++ b/src/com/android/tv/dialog/picker/PinPicker.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2018 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.dialog.picker;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v17.leanback.widget.picker.Picker;
+import android.support.v17.leanback.widget.picker.PickerColumn;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import java.util.ArrayList;
+import java.util.List;
+
+/** 4 digit picker */
+public final class PinPicker extends Picker {
+ // TODO(b/116144491): use leanback pin picker.
+
+ private final List<PickerColumn> mPickers = new ArrayList<>();
+ private OnClickListener mOnClickListener;
+
+ // the version of picker I link to does not have this constructor
+ public PinPicker(Context context, AttributeSet attributeSet) {
+ this(context, attributeSet, 0);
+ }
+
+ public PinPicker(Context context, AttributeSet attributeSet, int defStyleAttr) {
+ super(context, attributeSet, defStyleAttr);
+
+ for (int i = 0; i < 4; i++) {
+ PickerColumn pickerColumn = new PickerColumn();
+ pickerColumn.setMinValue(0);
+ pickerColumn.setMaxValue(9);
+ pickerColumn.setLabelFormat("%d");
+ mPickers.add(pickerColumn);
+ }
+ setSeparator(" ");
+ setColumns(mPickers);
+ setActivated(true);
+ setFocusable(true);
+ super.setOnClickListener(this::onClick);
+ }
+
+ public String getPinInput() {
+ String result = "";
+ try {
+ for (PickerColumn column : mPickers) {
+
+ result += column.getCurrentValue();
+ }
+ } catch (IllegalStateException e) {
+ result = "";
+ }
+ return result;
+ }
+
+ @Override
+ public void setOnClickListener(@Nullable OnClickListener l) {
+ mOnClickListener = l;
+ }
+
+ private void onClick(View v) {
+ int selectedColumn = getSelectedColumn();
+ int nextColumn = selectedColumn + 1;
+ // Only call the click listener if we are on the last column
+ // Otherwise move to the next column
+ if (nextColumn == getColumnsCount()) {
+ if (mOnClickListener != null) {
+ mOnClickListener.onClick(v);
+ }
+ } else {
+ setSelectedColumn(nextColumn);
+ onRequestFocusInDescendants(ViewGroup.FOCUS_FORWARD, null);
+ }
+ }
+
+ public void resetPinInput() {
+ setActivated(false);
+ for (int i = 0; i < 4; i++) {
+ setColumnValue(i, 0, true);
+ }
+ setSelectedColumn(0);
+ setActivated(true); // This resets the focus
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (event.getAction() == KeyEvent.ACTION_UP) {
+ int keyCode = event.getKeyCode();
+ int digit = digitFromKeyCode(keyCode);
+ if (digit != -1) {
+ int selectedColumn = getSelectedColumn();
+ setColumnValue(selectedColumn, digit, false);
+ int nextColumn = selectedColumn + 1;
+ if (nextColumn < getColumnsCount()) {
+ setSelectedColumn(nextColumn);
+ onRequestFocusInDescendants(ViewGroup.FOCUS_FORWARD, null);
+ } else {
+ callOnClick();
+ }
+ return true;
+ }
+ }
+ return super.dispatchKeyEvent(event);
+ }
+
+ @VisibleForTesting
+ static int digitFromKeyCode(int keyCode) {
+ if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
+ return keyCode - KeyEvent.KEYCODE_0;
+ } else if (keyCode >= KeyEvent.KEYCODE_NUMPAD_0 && keyCode <= KeyEvent.KEYCODE_NUMPAD_9) {
+ return keyCode - KeyEvent.KEYCODE_NUMPAD_0;
+ }
+ return -1;
+ }
+}
diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java
index 2b4ecbf5..0053650b 100644
--- a/src/com/android/tv/dvr/DvrDataManagerImpl.java
+++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java
@@ -16,7 +16,6 @@
package com.android.tv.dvr;
-import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.ContentUris;
@@ -49,21 +48,23 @@ import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.data.ScheduledRecording.RecordingState;
import com.android.tv.dvr.data.SeriesRecording;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddScheduleTask;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddSeriesRecordingTask;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDeleteScheduleTask;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDeleteSeriesRecordingTask;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryScheduleTask;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQuerySeriesRecordingTask;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateScheduleTask;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateSeriesRecordingTask;
+import com.android.tv.dvr.provider.DvrDbFuture.AddScheduleFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.AddSeriesRecordingFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.DeleteScheduleFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.DeleteSeriesRecordingFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.DvrQueryScheduleFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.DvrQuerySeriesRecordingFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.UpdateScheduleFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.UpdateSeriesRecordingFuture;
import com.android.tv.dvr.provider.DvrDbSync;
import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.AsyncDbTask.AsyncRecordedProgramQueryTask;
-import com.android.tv.util.Filter;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.TvUriMatcher;
+import com.google.common.base.Predicate;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -73,6 +74,7 @@ import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
/** DVR Data manager to handle recordings and schedules. */
@MainThread
@@ -106,8 +108,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
@Override
public void onChange(boolean selfChange, final @Nullable Uri uri) {
- RecordedProgramsQueryTask task =
- new RecordedProgramsQueryTask(mContext.getContentResolver(), uri);
+ RecordedProgramsQueryTask task = new RecordedProgramsQueryTask(uri);
task.executeOnDbThread();
mPendingTasks.add(task);
}
@@ -116,6 +117,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
private boolean mDvrLoadFinished;
private boolean mRecordedProgramLoadFinished;
private final Set<AsyncTask> mPendingTasks = new ArraySet<>();
+ private final Set<Future> mPendingDvrFuture = new ArraySet<>();
+ // TODO(b/79207567) make sure Future is not stopped at writing.
+ private final Set<Future> mNoStopFuture = new ArraySet<>();
private DvrDbSync mDbSync;
private RecordingStorageStatusManager mStorageStatusManager;
@@ -154,13 +158,27 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
};
+ private final FutureCallback<Void> removeFromSetOnCompletion =
+ new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(Void result) {
+ mNoStopFuture.remove(this);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ Log.w(TAG, "Failed to execute.", t);
+ mNoStopFuture.remove(this);
+ }
+ };
+
private static <T> List<T> moveElements(
- HashMap<Long, T> from, HashMap<Long, T> to, Filter<T> filter) {
+ HashMap<Long, T> from, HashMap<Long, T> to, Predicate<T> filter) {
List<T> moved = new ArrayList<>();
Iterator<Entry<Long, T>> iter = from.entrySet().iterator();
while (iter.hasNext()) {
Entry<Long, T> entry = iter.next();
- if (filter.filter(entry.getValue())) {
+ if (filter.apply(entry.getValue())) {
to.put(entry.getKey(), entry.getValue());
iter.remove();
moved.add(entry.getValue());
@@ -181,134 +199,143 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
public void start() {
mInputManager.addCallback(mInputCallback);
mStorageStatusManager.addListener(mStorageMountChangedListener);
- AsyncDvrQuerySeriesRecordingTask dvrQuerySeriesRecordingTask =
- new AsyncDvrQuerySeriesRecordingTask(mContext) {
- @Override
- protected void onCancelled(List<SeriesRecording> seriesRecordings) {
- mPendingTasks.remove(this);
- }
-
- @Override
- protected void onPostExecute(List<SeriesRecording> seriesRecordings) {
- mPendingTasks.remove(this);
- long maxId = 0;
- HashSet<String> seriesIds = new HashSet<>();
- for (SeriesRecording r : seriesRecordings) {
- if (SoftPreconditions.checkState(
- !seriesIds.contains(r.getSeriesId()),
- TAG,
- "Skip loading series recording with duplicate series ID: "
- + r)) {
- seriesIds.add(r.getSeriesId());
- if (isInputAvailable(r.getInputId())) {
- mSeriesRecordings.put(r.getId(), r);
- mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
- } else {
- mSeriesRecordingsForRemovedInput.put(r.getId(), r);
+ DvrQuerySeriesRecordingFuture dvrQuerySeriesRecordingTask =
+ new DvrQuerySeriesRecordingFuture(mContext);
+ ListenableFuture<List<SeriesRecording>> dvrQuerySeriesRecordingFuture =
+ dvrQuerySeriesRecordingTask.executeOnDbThread(
+ new FutureCallback<List<SeriesRecording>>() {
+ @Override
+ public void onSuccess(List<SeriesRecording> seriesRecordings) {
+ mPendingDvrFuture.remove(this);
+ long maxId = 0;
+ HashSet<String> seriesIds = new HashSet<>();
+ for (SeriesRecording r : seriesRecordings) {
+ if (SoftPreconditions.checkState(
+ !seriesIds.contains(r.getSeriesId()),
+ TAG,
+ "Skip loading series recording with duplicate series ID: "
+ + r)) {
+ seriesIds.add(r.getSeriesId());
+ if (isInputAvailable(r.getInputId())) {
+ mSeriesRecordings.put(r.getId(), r);
+ mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
+ } else {
+ mSeriesRecordingsForRemovedInput.put(r.getId(), r);
+ }
+ }
+ if (maxId < r.getId()) {
+ maxId = r.getId();
+ }
}
+ IdGenerator.SERIES_RECORDING.setMaxId(maxId);
}
- if (maxId < r.getId()) {
- maxId = r.getId();
- }
- }
- IdGenerator.SERIES_RECORDING.setMaxId(maxId);
- }
- };
- dvrQuerySeriesRecordingTask.executeOnDbThread();
- mPendingTasks.add(dvrQuerySeriesRecordingTask);
- AsyncDvrQueryScheduleTask dvrQueryScheduleTask =
- new AsyncDvrQueryScheduleTask(mContext) {
- @Override
- protected void onCancelled(List<ScheduledRecording> scheduledRecordings) {
- mPendingTasks.remove(this);
- }
- @SuppressLint("SwitchIntDef")
- @Override
- protected void onPostExecute(List<ScheduledRecording> result) {
- mPendingTasks.remove(this);
- long maxId = 0;
- int reasonNotStarted =
- ScheduledRecording
- .FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED;
- List<ScheduledRecording> toUpdate = new ArrayList<>();
- List<ScheduledRecording> toDelete = new ArrayList<>();
- for (ScheduledRecording r : result) {
- if (!isInputAvailable(r.getInputId())) {
- mScheduledRecordingsForRemovedInput.put(r.getId(), r);
- } else if (r.getState() == ScheduledRecording.STATE_RECORDING_DELETED) {
- getDeletedScheduleMap().put(r.getProgramId(), r);
- } else {
- mScheduledRecordings.put(r.getId(), r);
- if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
- mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
- }
- // Adjust the state of the schedules before DB loading is finished.
- switch (r.getState()) {
- case ScheduledRecording.STATE_RECORDING_IN_PROGRESS:
- if (r.getEndTimeMs() <= mClock.currentTimeMillis()) {
- int reason =
- ScheduledRecording.FAILED_REASON_NOT_FINISHED;
- toUpdate.add(
- ScheduledRecording.buildFrom(r)
- .setState(
- ScheduledRecording
- .STATE_RECORDING_FAILED)
- .setFailedReason(reason)
- .build());
- } else {
- toUpdate.add(
- ScheduledRecording.buildFrom(r)
- .setState(
- ScheduledRecording
- .STATE_RECORDING_NOT_STARTED)
- .build());
+ @Override
+ public void onFailure(Throwable t) {
+ Log.w(TAG, "Failed to load series recording.", t);
+ mPendingDvrFuture.remove(this);
+ }
+ });
+ mPendingDvrFuture.add(dvrQuerySeriesRecordingFuture);
+ DvrQueryScheduleFuture dvrQueryScheduleTask = new DvrQueryScheduleFuture(mContext);
+ ListenableFuture<List<ScheduledRecording>> dvrQueryScheduleFuture =
+ dvrQueryScheduleTask.executeOnDbThread(
+ new FutureCallback<List<ScheduledRecording>>() {
+ @Override
+ public void onSuccess(List<ScheduledRecording> result) {
+ mPendingDvrFuture.remove(this);
+ long maxId = 0;
+ int reasonNotStarted =
+ ScheduledRecording
+ .FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED;
+ List<ScheduledRecording> toUpdate = new ArrayList<>();
+ List<ScheduledRecording> toDelete = new ArrayList<>();
+ for (ScheduledRecording r : result) {
+ if (!isInputAvailable(r.getInputId())) {
+ mScheduledRecordingsForRemovedInput.put(r.getId(), r);
+ } else if (r.getState()
+ == ScheduledRecording.STATE_RECORDING_DELETED) {
+ getDeletedScheduleMap().put(r.getProgramId(), r);
+ } else {
+ mScheduledRecordings.put(r.getId(), r);
+ if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
+ mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
}
- break;
- case ScheduledRecording.STATE_RECORDING_NOT_STARTED:
- if (r.getEndTimeMs() <= mClock.currentTimeMillis()) {
- toUpdate.add(
- ScheduledRecording.buildFrom(r)
- .setState(
- ScheduledRecording
- .STATE_RECORDING_FAILED)
- .setFailedReason(reasonNotStarted)
- .build());
+ // Adjust the state of the schedules before DB loading is
+ // finished.
+ switch (r.getState()) {
+ case ScheduledRecording.STATE_RECORDING_IN_PROGRESS:
+ if (r.getEndTimeMs()
+ <= mClock.currentTimeMillis()) {
+ int reason =
+ ScheduledRecording
+ .FAILED_REASON_NOT_FINISHED;
+ toUpdate.add(
+ ScheduledRecording.buildFrom(r)
+ .setState(
+ ScheduledRecording
+ .STATE_RECORDING_FAILED)
+ .setFailedReason(reason)
+ .build());
+ } else {
+ toUpdate.add(
+ ScheduledRecording.buildFrom(r)
+ .setState(
+ ScheduledRecording
+ .STATE_RECORDING_NOT_STARTED)
+ .build());
+ }
+ break;
+ case ScheduledRecording.STATE_RECORDING_NOT_STARTED:
+ if (r.getEndTimeMs()
+ <= mClock.currentTimeMillis()) {
+ toUpdate.add(
+ ScheduledRecording.buildFrom(r)
+ .setState(
+ ScheduledRecording
+ .STATE_RECORDING_FAILED)
+ .setFailedReason(
+ reasonNotStarted)
+ .build());
+ }
+ break;
+ case ScheduledRecording.STATE_RECORDING_CANCELED:
+ toDelete.add(r);
+ break;
+ default: // fall out
}
- break;
- case ScheduledRecording.STATE_RECORDING_CANCELED:
- toDelete.add(r);
- break;
- default: // fall out
+ }
+ if (maxId < r.getId()) {
+ maxId = r.getId();
+ }
+ }
+ if (!toUpdate.isEmpty()) {
+ updateScheduledRecording(ScheduledRecording.toArray(toUpdate));
+ }
+ if (!toDelete.isEmpty()) {
+ removeScheduledRecording(ScheduledRecording.toArray(toDelete));
+ }
+ IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId);
+ if (mRecordedProgramLoadFinished) {
+ validateSeriesRecordings();
+ }
+ mDvrLoadFinished = true;
+ notifyDvrScheduleLoadFinished();
+ if (isInitialized()) {
+ mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this);
+ mDbSync.start();
+ SeriesRecordingScheduler.getInstance(mContext).start();
}
}
- if (maxId < r.getId()) {
- maxId = r.getId();
+
+ @Override
+ public void onFailure(Throwable t) {
+ Log.w(TAG, "Failed to load scheduled recording.", t);
+ mPendingDvrFuture.remove(this);
}
- }
- if (!toUpdate.isEmpty()) {
- updateScheduledRecording(ScheduledRecording.toArray(toUpdate));
- }
- if (!toDelete.isEmpty()) {
- removeScheduledRecording(ScheduledRecording.toArray(toDelete));
- }
- IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId);
- if (mRecordedProgramLoadFinished) {
- validateSeriesRecordings();
- }
- mDvrLoadFinished = true;
- notifyDvrScheduleLoadFinished();
- if (isInitialized()) {
- mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this);
- mDbSync.start();
- SeriesRecordingScheduler.getInstance(mContext).start();
- }
- }
- };
- dvrQueryScheduleTask.executeOnDbThread();
- mPendingTasks.add(dvrQueryScheduleTask);
- RecordedProgramsQueryTask mRecordedProgramQueryTask =
- new RecordedProgramsQueryTask(mContext.getContentResolver(), null);
+ });
+ mPendingDvrFuture.add(dvrQueryScheduleFuture);
+ RecordedProgramsQueryTask mRecordedProgramQueryTask = new RecordedProgramsQueryTask(null);
mRecordedProgramQueryTask.executeOnDbThread();
ContentResolver cr = mContext.getContentResolver();
cr.registerContentObserver(RecordedPrograms.CONTENT_URI, true, mContentObserver);
@@ -329,6 +356,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
i.remove();
task.cancel(true);
}
+ Iterator<Future> id = mPendingDvrFuture.iterator();
+ while (id.hasNext()) {
+ Future future = id.next();
+ id.remove();
+ future.cancel(true);
+ }
}
private void onRecordedProgramsLoadedFinished(Uri uri, List<RecordedProgram> recordedPrograms) {
@@ -607,7 +640,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (mDvrLoadFinished) {
notifyScheduledRecordingAdded(schedules);
}
- new AsyncAddScheduleTask(mContext).executeOnDbThread(schedules);
+ ListenableFuture addScheduleFuture =
+ new AddScheduleFuture(mContext)
+ .executeOnDbThread(removeFromSetOnCompletion, schedules);
+ mNoStopFuture.add(addScheduleFuture);
removeDeletedSchedules(schedules);
}
@@ -626,7 +662,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (mDvrLoadFinished) {
notifySeriesRecordingAdded(seriesRecordings);
}
- new AsyncAddSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings);
+ ListenableFuture addSeriesRecordingFuture =
+ new AddSeriesRecordingFuture(mContext)
+ .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings);
+ mNoStopFuture.add(addSeriesRecordingFuture);
}
@Override
@@ -683,12 +722,20 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
}
if (!schedulesToDelete.isEmpty()) {
- new AsyncDeleteScheduleTask(mContext)
- .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete));
+ ListenableFuture deleteScheduleFuture =
+ new DeleteScheduleFuture(mContext)
+ .executeOnDbThread(
+ removeFromSetOnCompletion,
+ ScheduledRecording.toArray(schedulesToDelete));
+ mNoStopFuture.add(deleteScheduleFuture);
}
if (!schedulesNotToDelete.isEmpty()) {
- new AsyncUpdateScheduleTask(mContext)
- .executeOnDbThread(ScheduledRecording.toArray(schedulesNotToDelete));
+ ListenableFuture updateScheduleFuture =
+ new UpdateScheduleFuture(mContext)
+ .executeOnDbThread(
+ removeFromSetOnCompletion,
+ ScheduledRecording.toArray(schedulesNotToDelete));
+ mNoStopFuture.add(updateScheduleFuture);
}
}
@@ -726,7 +773,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (mDvrLoadFinished) {
notifySeriesRecordingRemoved(seriesRecordings);
}
- new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings);
+ ListenableFuture deleteSeriesRecordingFuture =
+ new DeleteSeriesRecordingFuture(mContext)
+ .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings);
+ mNoStopFuture.add(deleteSeriesRecordingFuture);
removeDeletedSchedules(seriesRecordings);
}
@@ -778,7 +828,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
notifyScheduledRecordingStatusChanged(scheduleArray);
}
if (updateDb) {
- new AsyncUpdateScheduleTask(mContext).executeOnDbThread(scheduleArray);
+ ListenableFuture updateScheduleFuture =
+ new UpdateScheduleFuture(mContext)
+ .executeOnDbThread(removeFromSetOnCompletion, scheduleArray);
+ mNoStopFuture.add(updateScheduleFuture);
}
checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck);
removeDeletedSchedules(schedules);
@@ -802,7 +855,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (mDvrLoadFinished) {
notifySeriesRecordingChanged(seriesRecordings);
}
- new AsyncUpdateSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings);
+ ListenableFuture updateSeriesRecordingFuture =
+ new UpdateSeriesRecordingFuture(mContext)
+ .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings);
+ mNoStopFuture.add(updateSeriesRecordingFuture);
}
private boolean isInputAvailable(String inputId) {
@@ -820,8 +876,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
}
if (!schedulesToDelete.isEmpty()) {
- new AsyncDeleteScheduleTask(mContext)
- .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete));
+ ListenableFuture deleteScheduleFuture =
+ new DeleteScheduleFuture(mContext)
+ .executeOnDbThread(
+ removeFromSetOnCompletion,
+ ScheduledRecording.toArray(schedulesToDelete));
+ mNoStopFuture.add(deleteScheduleFuture);
}
}
@@ -841,8 +901,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
}
if (!schedulesToDelete.isEmpty()) {
- new AsyncDeleteScheduleTask(mContext)
- .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete));
+ ListenableFuture deleteScheduleFuture =
+ new DeleteScheduleFuture(mContext)
+ .executeOnDbThread(
+ removeFromSetOnCompletion,
+ ScheduledRecording.toArray(schedulesToDelete));
+ mNoStopFuture.add(deleteScheduleFuture);
}
}
@@ -852,38 +916,25 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
moveElements(
mScheduledRecordingsForRemovedInput,
mScheduledRecordings,
- new Filter<ScheduledRecording>() {
- @Override
- public boolean filter(ScheduledRecording r) {
- return r.getInputId().equals(inputId);
- }
- });
+ r -> r.getInputId().equals(inputId));
List<RecordedProgram> movedRecordedPrograms =
moveElements(
mRecordedProgramsForRemovedInput,
mRecordedPrograms,
- new Filter<RecordedProgram>() {
- @Override
- public boolean filter(RecordedProgram r) {
- return r.getInputId().equals(inputId);
- }
- });
+ r -> r.getInputId().equals(inputId));
List<SeriesRecording> removedSeriesRecordings = new ArrayList<>();
List<SeriesRecording> movedSeriesRecordings =
moveElements(
mSeriesRecordingsForRemovedInput,
mSeriesRecordings,
- new Filter<SeriesRecording>() {
- @Override
- public boolean filter(SeriesRecording r) {
- if (r.getInputId().equals(inputId)) {
- if (!isEmptySeriesRecording(r)) {
- return true;
- }
- removedSeriesRecordings.add(r);
+ r -> {
+ if (r.getInputId().equals(inputId)) {
+ if (!isEmptySeriesRecording(r)) {
+ return true;
}
- return false;
+ removedSeriesRecordings.add(r);
}
+ return false;
});
if (!movedSchedules.isEmpty()) {
for (ScheduledRecording schedule : movedSchedules) {
@@ -898,8 +949,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
for (SeriesRecording r : removedSeriesRecordings) {
mSeriesRecordingsForRemovedInput.remove(r.getId());
}
- new AsyncDeleteSeriesRecordingTask(mContext)
- .executeOnDbThread(SeriesRecording.toArray(removedSeriesRecordings));
+ ListenableFuture deleteSeriesRecordingFuture =
+ new DeleteSeriesRecordingFuture(mContext)
+ .executeOnDbThread(
+ removeFromSetOnCompletion,
+ SeriesRecording.toArray(removedSeriesRecordings));
+ mNoStopFuture.add(deleteSeriesRecordingFuture);
// Notify after all the data are moved.
if (!movedSchedules.isEmpty()) {
notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules));
@@ -918,32 +973,17 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
moveElements(
mScheduledRecordings,
mScheduledRecordingsForRemovedInput,
- new Filter<ScheduledRecording>() {
- @Override
- public boolean filter(ScheduledRecording r) {
- return r.getInputId().equals(inputId);
- }
- });
+ r -> r.getInputId().equals(inputId));
List<SeriesRecording> movedSeriesRecordings =
moveElements(
mSeriesRecordings,
mSeriesRecordingsForRemovedInput,
- new Filter<SeriesRecording>() {
- @Override
- public boolean filter(SeriesRecording r) {
- return r.getInputId().equals(inputId);
- }
- });
+ r -> r.getInputId().equals(inputId));
List<RecordedProgram> movedRecordedPrograms =
moveElements(
mRecordedPrograms,
mRecordedProgramsForRemovedInput,
- new Filter<RecordedProgram>() {
- @Override
- public boolean filter(RecordedProgram r) {
- return r.getInputId().equals(inputId);
- }
- });
+ r -> r.getInputId().equals(inputId));
if (!movedSchedules.isEmpty()) {
for (ScheduledRecording schedule : movedSchedules) {
mProgramId2ScheduledRecordings.remove(schedule.getProgramId());
@@ -1002,10 +1042,18 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
i.remove();
}
}
- new AsyncDeleteScheduleTask(mContext)
- .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete));
- new AsyncDeleteSeriesRecordingTask(mContext)
- .executeOnDbThread(SeriesRecording.toArray(seriesRecordingsToDelete));
+ ListenableFuture deleteScheduleFuture =
+ new DeleteScheduleFuture(mContext)
+ .executeOnDbThread(
+ removeFromSetOnCompletion,
+ ScheduledRecording.toArray(schedulesToDelete));
+ mNoStopFuture.add(deleteScheduleFuture);
+ ListenableFuture deleteSeriesRecordingFuture =
+ new DeleteSeriesRecordingFuture(mContext)
+ .executeOnDbThread(
+ removeFromSetOnCompletion,
+ SeriesRecording.toArray(seriesRecordingsToDelete));
+ mNoStopFuture.add(deleteSeriesRecordingFuture);
new AsyncDbTask<Void, Void, Void>(mDbExecutor) {
@Override
protected Void doInBackground(Void... params) {
@@ -1036,7 +1084,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
if (!removedSeriesRecordings.isEmpty()) {
SeriesRecording[] removed = SeriesRecording.toArray(removedSeriesRecordings);
- new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(removed);
+ ListenableFuture deleteSeriesRecordingFuture =
+ new DeleteSeriesRecordingFuture(mContext)
+ .executeOnDbThread(removeFromSetOnCompletion, removed);
+ mNoStopFuture.add(deleteSeriesRecordingFuture);
if (mDvrLoadFinished) {
notifySeriesRecordingRemoved(removed);
}
@@ -1046,8 +1097,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask {
private final Uri mUri;
- public RecordedProgramsQueryTask(ContentResolver contentResolver, Uri uri) {
- super(mDbExecutor, contentResolver, uri == null ? RecordedPrograms.CONTENT_URI : uri);
+ public RecordedProgramsQueryTask(Uri uri) {
+ super(mDbExecutor, mContext, uri == null ? RecordedPrograms.CONTENT_URI : uri);
mUri = uri;
}
diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java
index 63a245a3..cc9ad37a 100644
--- a/src/com/android/tv/dvr/DvrManager.java
+++ b/src/com/android/tv/dvr/DvrManager.java
@@ -29,6 +29,7 @@ import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.RemoteException;
+import android.support.annotation.AnyThread;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -441,14 +442,7 @@ public class DvrManager {
}
synchronized (mListener) {
for (final Entry<Listener, Handler> entry : mListener.entrySet()) {
- entry.getValue()
- .post(
- new Runnable() {
- @Override
- public void run() {
- entry.getKey().onStopRecordingRequested(recording);
- }
- });
+ entry.getValue().post(() -> entry.getKey().onStopRecordingRequested(recording));
}
}
}
@@ -484,26 +478,26 @@ public class DvrManager {
}
/** Removes the recorded program. It deletes the file if possible. */
- public void removeRecordedProgram(Uri recordedProgramUri) {
+ public void removeRecordedProgram(Uri recordedProgramUri, boolean deleteFile) {
if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
return;
}
- removeRecordedProgram(ContentUris.parseId(recordedProgramUri));
+ removeRecordedProgram(ContentUris.parseId(recordedProgramUri), deleteFile);
}
/** Removes the recorded program. It deletes the file if possible. */
- public void removeRecordedProgram(long recordedProgramId) {
+ public void removeRecordedProgram(long recordedProgramId, boolean deleteFile) {
if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
return;
}
RecordedProgram recordedProgram = mDataManager.getRecordedProgram(recordedProgramId);
if (recordedProgram != null) {
- removeRecordedProgram(recordedProgram);
+ removeRecordedProgram(recordedProgram, deleteFile);
}
}
/** Removes the recorded program. It deletes the file if possible. */
- public void removeRecordedProgram(final RecordedProgram recordedProgram) {
+ public void removeRecordedProgram(final RecordedProgram recordedProgram, boolean deleteFile) {
if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
return;
}
@@ -516,7 +510,7 @@ public class DvrManager {
@Override
protected void onPostExecute(Integer deletedCounts) {
- if (deletedCounts > 0) {
+ if (deletedCounts > 0 && deleteFile) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
@@ -529,7 +523,7 @@ public class DvrManager {
}.executeOnDbThread();
}
- public void removeRecordedPrograms(List<Long> recordedProgramIds) {
+ public void removeRecordedPrograms(List<Long> recordedProgramIds, boolean deleteFiles) {
final ArrayList<ContentProviderOperation> dbOperations = new ArrayList<>();
final List<Uri> dataUris = new ArrayList<>();
for (Long rId : recordedProgramIds) {
@@ -554,7 +548,7 @@ public class DvrManager {
@Override
protected void onPostExecute(Boolean success) {
- if (success) {
+ if (success && deleteFiles) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
@@ -829,37 +823,47 @@ public class DvrManager {
@WorkerThread
private void removeRecordedData(Uri dataUri) {
try {
- if (dataUri != null
- && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())
- && dataUri.getPath() != null) {
+ if (isFile(dataUri)) {
File recordedProgramPath = new File(dataUri.getPath());
if (!recordedProgramPath.exists()) {
if (DEBUG) Log.d(TAG, "File to delete not exist: " + recordedProgramPath);
} else {
- CommonUtils.deleteDirOrFile(recordedProgramPath);
- if (DEBUG) {
- Log.d(TAG, "Sucessfully deleted files of the recorded program: " + dataUri);
+ if (CommonUtils.deleteDirOrFile(recordedProgramPath)) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Successfully deleted files of the recorded program: "
+ + dataUri);
+ }
+ } else {
+ Log.w(TAG, "Unable to delete recording data at " + dataUri);
}
}
}
} catch (SecurityException e) {
- if (DEBUG) {
- Log.d(
- TAG,
- "To delete this recorded program, please manually delete video data at"
- + "\nadb shell rm -rf "
- + dataUri);
- }
+ Log.w(TAG, "Unable to delete recording data at " + dataUri, e);
}
}
+ @AnyThread
+ public static boolean isFromBundledInput(RecordedProgram mRecordedProgram) {
+ return CommonUtils.isInBundledPackageSet(mRecordedProgram.getPackageName());
+ }
+
+ @AnyThread
+ public static boolean isFile(Uri dataUri) {
+ return dataUri != null
+ && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())
+ && dataUri.getPath() != null;
+ }
+
/**
* Remove all the records related to the input.
*
* <p>Note that this should be called after the input was removed.
*/
public void forgetStorage(String inputId) {
- if (mDataManager.isInitialized()) {
+ if (mDataManager != null && mDataManager.isInitialized()) {
mDataManager.forgetStorage(inputId);
}
}
diff --git a/src/com/android/tv/dvr/DvrScheduleManager.java b/src/com/android/tv/dvr/DvrScheduleManager.java
index d5126b12..7202dce0 100644
--- a/src/com/android/tv/dvr/DvrScheduleManager.java
+++ b/src/com/android/tv/dvr/DvrScheduleManager.java
@@ -923,12 +923,8 @@ public class DvrScheduleManager {
List<ConflictInfo> result = new ArrayList<>(conflicts.values());
Collections.sort(
result,
- new Comparator<ConflictInfo>() {
- @Override
- public int compare(ConflictInfo lhs, ConflictInfo rhs) {
- return RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule);
- }
- });
+ (ConflictInfo lhs, ConflictInfo rhs) ->
+ RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule));
return result;
}
diff --git a/src/com/android/tv/dvr/DvrStorageStatusManager.java b/src/com/android/tv/dvr/DvrStorageStatusManager.java
index ed8d6903..dc347a9e 100644
--- a/src/com/android/tv/dvr/DvrStorageStatusManager.java
+++ b/src/com/android/tv/dvr/DvrStorageStatusManager.java
@@ -24,8 +24,9 @@ import android.media.tv.TvInputInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.RemoteException;
-import android.support.media.tv.TvContractCompat;
+import android.support.annotation.Nullable;
import android.util.Log;
+import androidx.tvprovider.media.tv.TvContractCompat;
import com.android.tv.TvSingletons;
import com.android.tv.common.recording.RecordingStorageStatusManager;
import com.android.tv.common.util.CommonUtils;
@@ -123,6 +124,8 @@ public class DvrStorageStatusManager extends RecordingStorageStatusManager {
}
}
+
+ @Nullable
private List<ContentProviderOperation> getDeleteOps() {
List<ContentProviderOperation> ops = new ArrayList<>();
@@ -165,6 +168,9 @@ public class DvrStorageStatusManager extends RecordingStorageStatusManager {
}
}
return ops;
+ } catch (Exception e) {
+ Log.w(TAG, "Error when getting delete ops at CleanUpDbTask", e);
+ return null;
}
}
}
diff --git a/src/com/android/tv/dvr/DvrTvView.java b/src/com/android/tv/dvr/DvrTvView.java
new file mode 100644
index 00000000..be1f418b
--- /dev/null
+++ b/src/com/android/tv/dvr/DvrTvView.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.dvr;
+
+import android.content.Context;
+import android.media.PlaybackParams;
+import android.media.session.PlaybackState;
+import android.media.tv.TvTrackInfo;
+import android.media.tv.TvView;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import com.android.tv.InputSessionManager;
+import com.android.tv.InputSessionManager.TvViewSession;
+import com.android.tv.TvSingletons;
+import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat;
+import com.android.tv.dvr.ui.playback.DvrPlayer;
+import com.android.tv.ui.AppLayerTvView;
+import com.android.tv.ui.api.TunableTvViewPlayingApi;
+import java.util.List;
+
+/**
+ * A {@link TvView} wrapper to handle events and TvView session.
+ */
+public class DvrTvView implements TunableTvViewPlayingApi {
+
+ private final AppLayerTvView mTvView;
+ private DvrPlayer mDvrPlayer;
+ private String mInputId;
+ private Uri mRecordedProgramUri;
+ private TvInputCallbackCompat mTvInputCallbackCompat;
+ private InputSessionManager mInputSessionManager;
+ private TvViewSession mSession;
+
+ public DvrTvView(Context context, AppLayerTvView tvView, DvrPlayer player) {
+ mTvView = tvView;
+ mDvrPlayer = player;
+ mInputSessionManager = TvSingletons.getSingletons(context).getInputSessionManager();
+ }
+
+ @Override
+ public boolean isPlaying() {
+ return mDvrPlayer.getPlaybackState() == PlaybackState.STATE_PLAYING;
+ }
+
+ @Override
+ public void setStreamVolume(float volume) {
+ mTvView.setStreamVolume(volume);
+ }
+
+ @Override
+ public void setTimeShiftListener(TimeShiftListener listener) {
+ // TimeShiftListener is never called from DvrTvView because TimeShift is always available
+ // and onRecordStartTimeChanged is not called during playback.
+ }
+
+ @Override
+ public boolean isTimeShiftAvailable() {
+ return true;
+ }
+
+ @Override
+ public void timeShiftPlay() {
+ if (mInputId != null && mRecordedProgramUri != null) {
+ mTvView.timeShiftPlay(mInputId, mRecordedProgramUri);
+ }
+ }
+
+ public void timeShiftPlay(String inputId, Uri recordedProgramUri) {
+ mInputId = inputId;
+ mRecordedProgramUri = recordedProgramUri;
+ mSession.timeShiftPlay(inputId, recordedProgramUri);
+ }
+
+ @Override
+ public void timeShiftPause() {
+ mTvView.timeShiftPause();
+ }
+
+ @Override
+ public void timeShiftRewind(int speed) {
+ PlaybackParams params = new PlaybackParams();
+ params.setSpeed(speed * -1);
+ mTvView.timeShiftSetPlaybackParams(params);
+ }
+
+ @Override
+ public void timeShiftFastForward(int speed) {
+ PlaybackParams params = new PlaybackParams();
+ params.setSpeed(speed);
+ mTvView.timeShiftSetPlaybackParams(params);
+ }
+
+ @Override
+ public void timeShiftSeekTo(long timeMs) {
+ mTvView.timeShiftSeekTo(timeMs);
+ }
+
+ @Override
+ public long timeShiftGetCurrentPositionMs() {
+ return mDvrPlayer.getPlaybackPosition();
+ }
+
+ public void setCaptionEnabled(boolean enabled) {
+ mTvView.setCaptionEnabled(enabled);
+ }
+
+ public void timeShiftResume() {
+ mTvView.timeShiftResume();
+ }
+
+ public void reset() {
+ mSession.reset();
+ }
+
+ public List<TvTrackInfo> getTracks(int type) {
+ return mTvView.getTracks(type);
+ }
+
+ public void selectTrack(int type, String trackId) {
+ mTvView.selectTrack(type, trackId);
+ }
+
+ public void timeShiftSetPlaybackParams(PlaybackParams params) {
+ mTvView.timeShiftSetPlaybackParams(params);
+ }
+
+ public void setTimeShiftPositionCallback(@Nullable TvView.TimeShiftPositionCallback callback) {
+ mTvView.setTimeShiftPositionCallback(callback);
+ }
+
+ public void setCallback(@Nullable TvInputCallbackCompat callback) {
+ mTvInputCallbackCompat = callback;
+ mTvView.setCallback(callback);
+ }
+
+ public void init() {
+ mSession = mInputSessionManager.createTvViewSession(mTvView, this, mTvInputCallbackCompat);
+ }
+
+ public void release() {
+ mInputSessionManager.releaseTvViewSession(mSession);
+ mInputSessionManager = null;
+ mDvrPlayer = null;
+ }
+}
diff --git a/src/com/android/tv/dvr/data/RecordedProgram.java b/src/com/android/tv/dvr/data/RecordedProgram.java
index e1fbca8c..899e65ac 100644
--- a/src/com/android/tv/dvr/data/RecordedProgram.java
+++ b/src/com/android/tv/dvr/data/RecordedProgram.java
@@ -22,31 +22,38 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.media.tv.TvContentRating;
-import android.media.tv.TvContract;
+import android.media.tv.TvContract.Programs.Genres;
import android.media.tv.TvContract.RecordedPrograms;
import android.net.Uri;
import android.os.Build;
+import android.support.annotation.CheckResult;
import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
import android.text.TextUtils;
+import android.util.Log;
import com.android.tv.common.R;
import com.android.tv.common.TvContentRatingCache;
+import com.android.tv.common.data.RecordedProgramState;
import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.util.StringUtils;
import com.android.tv.data.BaseProgram;
import com.android.tv.data.GenreItems;
import com.android.tv.data.InternalDataUtils;
-import java.util.Arrays;
+import com.android.tv.util.TvProviderUtils;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
import java.util.Collection;
import java.util.Comparator;
-import java.util.Objects;
import java.util.concurrent.TimeUnit;
/** Immutable instance of {@link android.media.tv.TvContract.RecordedPrograms}. */
@TargetApi(Build.VERSION_CODES.N)
-public class RecordedProgram extends BaseProgram {
+@AutoValue
+public abstract class RecordedProgram extends BaseProgram {
public static final int ID_NOT_SET = -1;
+ private static final String TAG = "RecordedProgram";
public static final String[] PROJECTION = {
- // These are in exactly the order listed in RecordedPrograms
RecordedPrograms._ID,
RecordedPrograms.COLUMN_PACKAGE_NAME,
RecordedPrograms.COLUMN_INPUT_ID,
@@ -73,10 +80,6 @@ public class RecordedProgram extends BaseProgram {
RecordedPrograms.COLUMN_RECORDING_DATA_BYTES,
RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS,
- RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
- RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
- RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
- RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
RecordedPrograms.COLUMN_VERSION_NUMBER,
RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
};
@@ -89,834 +92,372 @@ public class RecordedProgram extends BaseProgram {
.setPackageName(cursor.getString(index++))
.setInputId(cursor.getString(index++))
.setChannelId(cursor.getLong(index++))
- .setTitle(cursor.getString(index++))
- .setSeasonNumber(cursor.getString(index++))
- .setSeasonTitle(cursor.getString(index++))
- .setEpisodeNumber(cursor.getString(index++))
- .setEpisodeTitle(cursor.getString(index++))
+ .setTitle(StringUtils.nullToEmpty(cursor.getString(index++)))
+ .setSeasonNumber(StringUtils.nullToEmpty(cursor.getString(index++)))
+ .setSeasonTitle(StringUtils.nullToEmpty(cursor.getString(index++)))
+ .setEpisodeNumber(StringUtils.nullToEmpty(cursor.getString(index++)))
+ .setEpisodeTitle(StringUtils.nullToEmpty(cursor.getString(index++)))
.setStartTimeUtcMillis(cursor.getLong(index++))
.setEndTimeUtcMillis(cursor.getLong(index++))
.setBroadcastGenres(cursor.getString(index++))
.setCanonicalGenres(cursor.getString(index++))
- .setShortDescription(cursor.getString(index++))
- .setLongDescription(cursor.getString(index++))
+ .setDescription(StringUtils.nullToEmpty(cursor.getString(index++)))
+ .setLongDescription(StringUtils.nullToEmpty(cursor.getString(index++)))
.setVideoWidth(cursor.getInt(index++))
.setVideoHeight(cursor.getInt(index++))
- .setAudioLanguage(cursor.getString(index++))
+ .setAudioLanguage(StringUtils.nullToEmpty(cursor.getString(index++)))
.setContentRatings(
TvContentRatingCache.getInstance()
.getRatings(cursor.getString(index++)))
- .setPosterArtUri(cursor.getString(index++))
- .setThumbnailUri(cursor.getString(index++))
+ .setPosterArtUri(StringUtils.nullToEmpty(cursor.getString(index++)))
+ .setThumbnailUri(StringUtils.nullToEmpty(cursor.getString(index++)))
.setSearchable(cursor.getInt(index++) == 1)
.setDataUri(cursor.getString(index++))
.setDataBytes(cursor.getLong(index++))
.setDurationMillis(cursor.getLong(index++))
.setExpireTimeUtcMillis(cursor.getLong(index++))
- .setInternalProviderFlag1(cursor.getInt(index++))
- .setInternalProviderFlag2(cursor.getInt(index++))
- .setInternalProviderFlag3(cursor.getInt(index++))
- .setInternalProviderFlag4(cursor.getInt(index++))
.setVersionNumber(cursor.getInt(index++));
- if (CommonUtils.isInBundledPackageSet(builder.mPackageName)) {
+ if (CommonUtils.isInBundledPackageSet(builder.getPackageName())) {
InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder);
}
+ index++;
+ if (TvProviderUtils.getRecordedProgramHasSeriesIdColumn()) {
+ builder.setSeriesId(StringUtils.nullToEmpty(cursor.getString(index++)));
+ }
+ if (TvProviderUtils.getRecordedProgramHasStateColumn()) {
+ builder.setState(cursor.getString(index++));
+ }
return builder.build();
}
- public static ContentValues toValues(RecordedProgram recordedProgram) {
+ @WorkerThread
+ public static ContentValues toValues(Context context, RecordedProgram recordedProgram) {
ContentValues values = new ContentValues();
- if (recordedProgram.mId != ID_NOT_SET) {
- values.put(RecordedPrograms._ID, recordedProgram.mId);
+ if (recordedProgram.getId() != ID_NOT_SET) {
+ values.put(RecordedPrograms._ID, recordedProgram.getId());
}
- values.put(RecordedPrograms.COLUMN_INPUT_ID, recordedProgram.mInputId);
- values.put(RecordedPrograms.COLUMN_CHANNEL_ID, recordedProgram.mChannelId);
- values.put(RecordedPrograms.COLUMN_TITLE, recordedProgram.mTitle);
- values.put(RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, recordedProgram.mSeasonNumber);
- values.put(RecordedPrograms.COLUMN_SEASON_TITLE, recordedProgram.mSeasonTitle);
- values.put(RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, recordedProgram.mEpisodeNumber);
- values.put(RecordedPrograms.COLUMN_EPISODE_TITLE, recordedProgram.mTitle);
+ values.put(RecordedPrograms.COLUMN_INPUT_ID, recordedProgram.getInputId());
+ values.put(RecordedPrograms.COLUMN_CHANNEL_ID, recordedProgram.getChannelId());
+ values.put(RecordedPrograms.COLUMN_TITLE, recordedProgram.getTitle());
+ values.put(
+ RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, recordedProgram.getSeasonNumber());
+ values.put(RecordedPrograms.COLUMN_SEASON_TITLE, recordedProgram.getSeasonTitle());
+ values.put(
+ RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, recordedProgram.getEpisodeNumber());
+ values.put(RecordedPrograms.COLUMN_EPISODE_TITLE, recordedProgram.getEpisodeTitle());
+ values.put(
+ RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS,
+ recordedProgram.getStartTimeUtcMillis());
values.put(
- RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, recordedProgram.mStartTimeUtcMillis);
- values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, recordedProgram.mEndTimeUtcMillis);
+ RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, recordedProgram.getEndTimeUtcMillis());
values.put(
RecordedPrograms.COLUMN_BROADCAST_GENRE,
- safeEncode(recordedProgram.mBroadcastGenres));
+ safeEncode(recordedProgram.getBroadcastGenres()));
values.put(
RecordedPrograms.COLUMN_CANONICAL_GENRE,
- safeEncode(recordedProgram.mCanonicalGenres));
- values.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION, recordedProgram.mShortDescription);
- values.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION, recordedProgram.mLongDescription);
- if (recordedProgram.mVideoWidth == 0) {
+ safeEncode(recordedProgram.getCanonicalGenres()));
+ values.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION, recordedProgram.getDescription());
+ values.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION, recordedProgram.getLongDescription());
+ if (recordedProgram.getVideoWidth() == 0) {
values.putNull(RecordedPrograms.COLUMN_VIDEO_WIDTH);
} else {
- values.put(RecordedPrograms.COLUMN_VIDEO_WIDTH, recordedProgram.mVideoWidth);
+ values.put(RecordedPrograms.COLUMN_VIDEO_WIDTH, recordedProgram.getVideoWidth());
}
- if (recordedProgram.mVideoHeight == 0) {
+ if (recordedProgram.getVideoHeight() == 0) {
values.putNull(RecordedPrograms.COLUMN_VIDEO_HEIGHT);
} else {
- values.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, recordedProgram.mVideoHeight);
+ values.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, recordedProgram.getVideoHeight());
}
- values.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, recordedProgram.mAudioLanguage);
+ values.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, recordedProgram.getAudioLanguage());
values.put(
RecordedPrograms.COLUMN_CONTENT_RATING,
- TvContentRatingCache.contentRatingsToString(recordedProgram.mContentRatings));
- values.put(RecordedPrograms.COLUMN_POSTER_ART_URI, recordedProgram.mPosterArtUri);
- values.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, recordedProgram.mThumbnailUri);
- values.put(RecordedPrograms.COLUMN_SEARCHABLE, recordedProgram.mSearchable ? 1 : 0);
+ TvContentRatingCache.contentRatingsToString(recordedProgram.getContentRatings()));
+ values.put(RecordedPrograms.COLUMN_POSTER_ART_URI, recordedProgram.getPosterArtUri());
+ values.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, recordedProgram.getThumbnailUri());
+ values.put(RecordedPrograms.COLUMN_SEARCHABLE, recordedProgram.isSearchable() ? 1 : 0);
values.put(
- RecordedPrograms.COLUMN_RECORDING_DATA_URI, safeToString(recordedProgram.mDataUri));
- values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, recordedProgram.mDataBytes);
+ RecordedPrograms.COLUMN_RECORDING_DATA_URI,
+ safeToString(recordedProgram.getDataUri()));
+ values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, recordedProgram.getDataBytes());
values.put(
- RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, recordedProgram.mDurationMillis);
+ RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
+ recordedProgram.getDurationMillis());
values.put(
RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS,
- recordedProgram.mExpireTimeUtcMillis);
+ recordedProgram.getExpireTimeUtcMillis());
values.put(
RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
InternalDataUtils.serializeInternalProviderData(recordedProgram));
- values.put(
- RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
- recordedProgram.mInternalProviderFlag1);
- values.put(
- RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
- recordedProgram.mInternalProviderFlag2);
- values.put(
- RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
- recordedProgram.mInternalProviderFlag3);
- values.put(
- RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
- recordedProgram.mInternalProviderFlag4);
- values.put(RecordedPrograms.COLUMN_VERSION_NUMBER, recordedProgram.mVersionNumber);
+ values.put(RecordedPrograms.COLUMN_VERSION_NUMBER, recordedProgram.getVersionNumber());
+ if (TvProviderUtils.checkSeriesIdColumn(context, RecordedPrograms.CONTENT_URI)) {
+ values.put(COLUMN_SERIES_ID, recordedProgram.getSeriesId());
+ }
+ if (TvProviderUtils.checkStateColumn(context, RecordedPrograms.CONTENT_URI)) {
+ values.put(COLUMN_STATE, recordedProgram.getState().toString());
+ }
return values;
}
- public static class Builder {
- private long mId = ID_NOT_SET;
- private String mPackageName;
- private String mInputId;
- private long mChannelId;
- private String mTitle;
- private String mSeriesId;
- private String mSeasonNumber;
- private String mSeasonTitle;
- private String mEpisodeNumber;
- private String mEpisodeTitle;
- private long mStartTimeUtcMillis;
- private long mEndTimeUtcMillis;
- private String[] mBroadcastGenres;
- private String[] mCanonicalGenres;
- private String mShortDescription;
- private String mLongDescription;
- private int mVideoWidth;
- private int mVideoHeight;
- private String mAudioLanguage;
- private TvContentRating[] mContentRatings;
- private String mPosterArtUri;
- private String mThumbnailUri;
- private boolean mSearchable = true;
- private Uri mDataUri;
- private long mDataBytes;
- private long mDurationMillis;
- private long mExpireTimeUtcMillis;
- private int mInternalProviderFlag1;
- private int mInternalProviderFlag2;
- private int mInternalProviderFlag3;
- private int mInternalProviderFlag4;
- private int mVersionNumber;
-
- public Builder setId(long id) {
- mId = id;
- return this;
- }
+ /** Builder for {@link RecordedProgram}s. */
+ @AutoValue.Builder
+ public abstract static class Builder {
- public Builder setPackageName(String packageName) {
- mPackageName = packageName;
- return this;
- }
+ public abstract Builder setId(long id);
- public Builder setInputId(String inputId) {
- mInputId = inputId;
- return this;
- }
+ public abstract Builder setPackageName(String packageName);
- public Builder setChannelId(long channelId) {
- mChannelId = channelId;
- return this;
- }
+ abstract String getPackageName();
- public Builder setTitle(String title) {
- mTitle = title;
- return this;
- }
+ public abstract Builder setInputId(String inputId);
- public Builder setSeriesId(String seriesId) {
- mSeriesId = seriesId;
- return this;
- }
+ public abstract Builder setChannelId(long channelId);
- public Builder setSeasonNumber(String seasonNumber) {
- mSeasonNumber = seasonNumber;
- return this;
- }
+ abstract String getTitle();
- public Builder setSeasonTitle(String seasonTitle) {
- mSeasonTitle = seasonTitle;
- return this;
- }
+ public abstract Builder setTitle(String title);
- public Builder setEpisodeNumber(String episodeNumber) {
- mEpisodeNumber = episodeNumber;
- return this;
- }
+ abstract String getSeriesId();
- public Builder setEpisodeTitle(String episodeTitle) {
- mEpisodeTitle = episodeTitle;
- return this;
- }
+ public abstract Builder setSeriesId(String seriesId);
- public Builder setStartTimeUtcMillis(long startTimeUtcMillis) {
- mStartTimeUtcMillis = startTimeUtcMillis;
- return this;
- }
+ public abstract Builder setSeasonNumber(String seasonNumber);
- public Builder setEndTimeUtcMillis(long endTimeUtcMillis) {
- mEndTimeUtcMillis = endTimeUtcMillis;
- return this;
- }
+ public abstract Builder setSeasonTitle(String seasonTitle);
+
+ @Nullable
+ abstract String getEpisodeNumber();
+
+ public abstract Builder setEpisodeNumber(String episodeNumber);
- public Builder setBroadcastGenres(String broadcastGenres) {
- if (TextUtils.isEmpty(broadcastGenres)) {
- mBroadcastGenres = null;
- return this;
+ public abstract Builder setEpisodeTitle(String episodeTitle);
+
+ public abstract Builder setStartTimeUtcMillis(long startTimeUtcMillis);
+
+ public abstract Builder setEndTimeUtcMillis(long endTimeUtcMillis);
+
+ public abstract Builder setState(RecordedProgramState state);
+
+ public Builder setState(@Nullable String state) {
+
+ if (!TextUtils.isEmpty(state)) {
+ try {
+ return setState(RecordedProgramState.valueOf(state));
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Unknown recording state " + state, e);
+ }
}
- return setBroadcastGenres(TvContract.Programs.Genres.decode(broadcastGenres));
+ return setState(RecordedProgramState.NOT_SET);
}
- private Builder setBroadcastGenres(String[] broadcastGenres) {
- mBroadcastGenres = broadcastGenres;
- return this;
+ public Builder setBroadcastGenres(@Nullable String broadcastGenres) {
+ return setBroadcastGenres(
+ TextUtils.isEmpty(broadcastGenres)
+ ? ImmutableList.of()
+ : ImmutableList.copyOf(Genres.decode(broadcastGenres)));
}
+ public abstract Builder setBroadcastGenres(ImmutableList<String> broadcastGenres);
+
public Builder setCanonicalGenres(String canonicalGenres) {
- if (TextUtils.isEmpty(canonicalGenres)) {
- mCanonicalGenres = null;
- return this;
- }
- return setCanonicalGenres(TvContract.Programs.Genres.decode(canonicalGenres));
+ return setCanonicalGenres(
+ TextUtils.isEmpty(canonicalGenres)
+ ? ImmutableList.of()
+ : ImmutableList.copyOf(Genres.decode(canonicalGenres)));
}
- private Builder setCanonicalGenres(String[] canonicalGenres) {
- mCanonicalGenres = canonicalGenres;
- return this;
- }
+ public abstract Builder setCanonicalGenres(ImmutableList<String> canonicalGenres);
- public Builder setShortDescription(String shortDescription) {
- mShortDescription = shortDescription;
- return this;
- }
+ public abstract Builder setDescription(String shortDescription);
- public Builder setLongDescription(String longDescription) {
- mLongDescription = longDescription;
- return this;
- }
+ public abstract Builder setLongDescription(String longDescription);
- public Builder setVideoWidth(int videoWidth) {
- mVideoWidth = videoWidth;
- return this;
- }
+ public abstract Builder setVideoWidth(int videoWidth);
- public Builder setVideoHeight(int videoHeight) {
- mVideoHeight = videoHeight;
- return this;
- }
+ public abstract Builder setVideoHeight(int videoHeight);
- public Builder setAudioLanguage(String audioLanguage) {
- mAudioLanguage = audioLanguage;
- return this;
- }
+ public abstract Builder setAudioLanguage(String audioLanguage);
- public Builder setContentRatings(TvContentRating[] contentRatings) {
- mContentRatings = contentRatings;
- return this;
- }
+ public abstract Builder setContentRatings(ImmutableList<TvContentRating> contentRatings);
- private Uri toUri(String uriString) {
+ private Uri toUri(@Nullable String uriString) {
try {
return uriString == null ? null : Uri.parse(uriString);
} catch (Exception e) {
- return null;
+ return Uri.EMPTY;
}
}
- public Builder setPosterArtUri(String posterArtUri) {
- mPosterArtUri = posterArtUri;
- return this;
- }
+ public abstract Builder setPosterArtUri(String posterArtUri);
- public Builder setThumbnailUri(String thumbnailUri) {
- mThumbnailUri = thumbnailUri;
- return this;
- }
+ public abstract Builder setThumbnailUri(String thumbnailUri);
- public Builder setSearchable(boolean searchable) {
- mSearchable = searchable;
- return this;
- }
+ public abstract Builder setSearchable(boolean searchable);
- public Builder setDataUri(String dataUri) {
+ public Builder setDataUri(@Nullable String dataUri) {
return setDataUri(toUri(dataUri));
}
- public Builder setDataUri(Uri dataUri) {
- mDataUri = dataUri;
- return this;
- }
-
- public Builder setDataBytes(long dataBytes) {
- mDataBytes = dataBytes;
- return this;
- }
-
- public Builder setDurationMillis(long durationMillis) {
- mDurationMillis = durationMillis;
- return this;
- }
-
- public Builder setExpireTimeUtcMillis(long expireTimeUtcMillis) {
- mExpireTimeUtcMillis = expireTimeUtcMillis;
- return this;
- }
+ public abstract Builder setDataUri(Uri dataUri);
- public Builder setInternalProviderFlag1(int internalProviderFlag1) {
- mInternalProviderFlag1 = internalProviderFlag1;
- return this;
- }
+ public abstract Builder setDataBytes(long dataBytes);
- public Builder setInternalProviderFlag2(int internalProviderFlag2) {
- mInternalProviderFlag2 = internalProviderFlag2;
- return this;
- }
+ public abstract Builder setDurationMillis(long durationMillis);
- public Builder setInternalProviderFlag3(int internalProviderFlag3) {
- mInternalProviderFlag3 = internalProviderFlag3;
- return this;
- }
+ public abstract Builder setExpireTimeUtcMillis(long expireTimeUtcMillis);
- public Builder setInternalProviderFlag4(int internalProviderFlag4) {
- mInternalProviderFlag4 = internalProviderFlag4;
- return this;
- }
+ public abstract Builder setVersionNumber(int versionNumber);
- public Builder setVersionNumber(int versionNumber) {
- mVersionNumber = versionNumber;
- return this;
- }
+ abstract RecordedProgram autoBuild();
public RecordedProgram build() {
- if (TextUtils.isEmpty(mTitle)) {
+ if (TextUtils.isEmpty(getTitle())) {
// If title is null, series cannot be generated for this program.
setSeriesId(null);
- } else if (TextUtils.isEmpty(mSeriesId) && !TextUtils.isEmpty(mEpisodeNumber)) {
+ } else if (TextUtils.isEmpty(getSeriesId()) && !TextUtils.isEmpty(getEpisodeNumber())) {
// If series ID is not set, generate it for the episodic program of other TV input.
- setSeriesId(BaseProgram.generateSeriesId(mPackageName, mTitle));
+ setSeriesId(BaseProgram.generateSeriesId(getPackageName(), getTitle()));
}
- return new RecordedProgram(
- mId,
- mPackageName,
- mInputId,
- mChannelId,
- mTitle,
- mSeriesId,
- mSeasonNumber,
- mSeasonTitle,
- mEpisodeNumber,
- mEpisodeTitle,
- mStartTimeUtcMillis,
- mEndTimeUtcMillis,
- mBroadcastGenres,
- mCanonicalGenres,
- mShortDescription,
- mLongDescription,
- mVideoWidth,
- mVideoHeight,
- mAudioLanguage,
- mContentRatings,
- mPosterArtUri,
- mThumbnailUri,
- mSearchable,
- mDataUri,
- mDataBytes,
- mDurationMillis,
- mExpireTimeUtcMillis,
- mInternalProviderFlag1,
- mInternalProviderFlag2,
- mInternalProviderFlag3,
- mInternalProviderFlag4,
- mVersionNumber);
+ return (autoBuild());
}
}
public static Builder builder() {
- return new Builder();
- }
-
- public static Builder buildFrom(RecordedProgram orig) {
- return builder()
- .setId(orig.getId())
- .setPackageName(orig.getPackageName())
- .setInputId(orig.getInputId())
- .setChannelId(orig.getChannelId())
- .setTitle(orig.getTitle())
- .setSeriesId(orig.getSeriesId())
- .setSeasonNumber(orig.getSeasonNumber())
- .setSeasonTitle(orig.getSeasonTitle())
- .setEpisodeNumber(orig.getEpisodeNumber())
- .setEpisodeTitle(orig.getEpisodeTitle())
- .setStartTimeUtcMillis(orig.getStartTimeUtcMillis())
- .setEndTimeUtcMillis(orig.getEndTimeUtcMillis())
- .setBroadcastGenres(orig.getBroadcastGenres())
- .setCanonicalGenres(orig.getCanonicalGenres())
- .setShortDescription(orig.getDescription())
- .setLongDescription(orig.getLongDescription())
- .setVideoWidth(orig.getVideoWidth())
- .setVideoHeight(orig.getVideoHeight())
- .setAudioLanguage(orig.getAudioLanguage())
- .setContentRatings(orig.getContentRatings())
- .setPosterArtUri(orig.getPosterArtUri())
- .setThumbnailUri(orig.getThumbnailUri())
- .setSearchable(orig.isSearchable())
- .setInternalProviderFlag1(orig.getInternalProviderFlag1())
- .setInternalProviderFlag2(orig.getInternalProviderFlag2())
- .setInternalProviderFlag3(orig.getInternalProviderFlag3())
- .setInternalProviderFlag4(orig.getInternalProviderFlag4())
- .setVersionNumber(orig.getVersionNumber());
+ return new AutoValue_RecordedProgram.Builder()
+ .setId(ID_NOT_SET)
+ .setChannelId(ID_NOT_SET)
+ .setAudioLanguage("")
+ .setBroadcastGenres("")
+ .setCanonicalGenres("")
+ .setContentRatings(ImmutableList.of())
+ .setDataUri("")
+ .setDurationMillis(0)
+ .setDescription("")
+ .setDataBytes(0)
+ .setLongDescription("")
+ .setEndTimeUtcMillis(0)
+ .setEpisodeNumber("")
+ .setEpisodeTitle("")
+ .setExpireTimeUtcMillis(0)
+ .setPackageName("")
+ .setPosterArtUri("")
+ .setSeasonNumber("")
+ .setSeasonTitle("")
+ .setSearchable(false)
+ .setSeriesId("")
+ .setStartTimeUtcMillis(0)
+ .setState(RecordedProgramState.NOT_SET)
+ .setThumbnailUri("")
+ .setTitle("")
+ .setVersionNumber(0)
+ .setVideoHeight(0)
+ .setVideoWidth(0);
}
public static final Comparator<RecordedProgram> START_TIME_THEN_ID_COMPARATOR =
- new Comparator<RecordedProgram>() {
- @Override
- public int compare(RecordedProgram lhs, RecordedProgram rhs) {
- int res =
- Long.compare(lhs.getStartTimeUtcMillis(), rhs.getStartTimeUtcMillis());
- if (res != 0) {
- return res;
- }
- return Long.compare(lhs.mId, rhs.mId);
+ (RecordedProgram lhs, RecordedProgram rhs) -> {
+ int res = Long.compare(lhs.getStartTimeUtcMillis(), rhs.getStartTimeUtcMillis());
+ if (res != 0) {
+ return res;
}
+ return Long.compare(lhs.getId(), rhs.getId());
};
private static final long CLIPPED_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5);
- private final long mId;
- private final String mPackageName;
- private final String mInputId;
- private final long mChannelId;
- private final String mTitle;
- private final String mSeriesId;
- private final String mSeasonNumber;
- private final String mSeasonTitle;
- private final String mEpisodeNumber;
- private final String mEpisodeTitle;
- private final long mStartTimeUtcMillis;
- private final long mEndTimeUtcMillis;
- private final String[] mBroadcastGenres;
- private final String[] mCanonicalGenres;
- private final String mShortDescription;
- private final String mLongDescription;
- private final int mVideoWidth;
- private final int mVideoHeight;
- private final String mAudioLanguage;
- private final TvContentRating[] mContentRatings;
- private final String mPosterArtUri;
- private final String mThumbnailUri;
- private final boolean mSearchable;
- private final Uri mDataUri;
- private final long mDataBytes;
- private final long mDurationMillis;
- private final long mExpireTimeUtcMillis;
- private final int mInternalProviderFlag1;
- private final int mInternalProviderFlag2;
- private final int mInternalProviderFlag3;
- private final int mInternalProviderFlag4;
- private final int mVersionNumber;
-
- private RecordedProgram(
- long id,
- String packageName,
- String inputId,
- long channelId,
- String title,
- String seriesId,
- String seasonNumber,
- String seasonTitle,
- String episodeNumber,
- String episodeTitle,
- long startTimeUtcMillis,
- long endTimeUtcMillis,
- String[] broadcastGenres,
- String[] canonicalGenres,
- String shortDescription,
- String longDescription,
- int videoWidth,
- int videoHeight,
- String audioLanguage,
- TvContentRating[] contentRatings,
- String posterArtUri,
- String thumbnailUri,
- boolean searchable,
- Uri dataUri,
- long dataBytes,
- long durationMillis,
- long expireTimeUtcMillis,
- int internalProviderFlag1,
- int internalProviderFlag2,
- int internalProviderFlag3,
- int internalProviderFlag4,
- int versionNumber) {
- mId = id;
- mPackageName = packageName;
- mInputId = inputId;
- mChannelId = channelId;
- mTitle = title;
- mSeriesId = seriesId;
- mSeasonNumber = seasonNumber;
- mSeasonTitle = seasonTitle;
- mEpisodeNumber = episodeNumber;
- mEpisodeTitle = episodeTitle;
- mStartTimeUtcMillis = startTimeUtcMillis;
- mEndTimeUtcMillis = endTimeUtcMillis;
- mBroadcastGenres = broadcastGenres;
- mCanonicalGenres = canonicalGenres;
- mShortDescription = shortDescription;
- mLongDescription = longDescription;
- mVideoWidth = videoWidth;
- mVideoHeight = videoHeight;
-
- mAudioLanguage = audioLanguage;
- mContentRatings = contentRatings;
- mPosterArtUri = posterArtUri;
- mThumbnailUri = thumbnailUri;
- mSearchable = searchable;
- mDataUri = dataUri;
- mDataBytes = dataBytes;
- mDurationMillis = durationMillis;
- mExpireTimeUtcMillis = expireTimeUtcMillis;
- mInternalProviderFlag1 = internalProviderFlag1;
- mInternalProviderFlag2 = internalProviderFlag2;
- mInternalProviderFlag3 = internalProviderFlag3;
- mInternalProviderFlag4 = internalProviderFlag4;
- mVersionNumber = versionNumber;
- }
-
- public String getAudioLanguage() {
- return mAudioLanguage;
- }
+ public abstract String getAudioLanguage();
- public String[] getBroadcastGenres() {
- return mBroadcastGenres;
- }
+ public abstract ImmutableList<String> getBroadcastGenres();
- public String[] getCanonicalGenres() {
- return mCanonicalGenres;
- }
+ public abstract ImmutableList<String> getCanonicalGenres();
/** Returns array of canonical genre ID's for this recorded program. */
@Override
public int[] getCanonicalGenreIds() {
- if (mCanonicalGenres == null) {
- return null;
- }
- int[] genreIds = new int[mCanonicalGenres.length];
- for (int i = 0; i < mCanonicalGenres.length; i++) {
- genreIds[i] = GenreItems.getId(mCanonicalGenres[i]);
+
+ ImmutableList<String> canonicalGenres = getCanonicalGenres();
+ int[] genreIds = new int[getCanonicalGenres().size()];
+ for (int i = 0; i < canonicalGenres.size(); i++) {
+ genreIds[i] = GenreItems.getId(canonicalGenres.get(i));
}
return genreIds;
}
- @Override
- public long getChannelId() {
- return mChannelId;
- }
-
- @Nullable
- @Override
- public TvContentRating[] getContentRatings() {
- return mContentRatings;
- }
-
- public Uri getDataUri() {
- return mDataUri;
- }
-
- public long getDataBytes() {
- return mDataBytes;
- }
-
- @Override
- public long getDurationMillis() {
- return mDurationMillis;
- }
-
- @Override
- public long getEndTimeUtcMillis() {
- return mEndTimeUtcMillis;
- }
-
- @Override
- public String getEpisodeNumber() {
- return mEpisodeNumber;
- }
+ public abstract Uri getDataUri();
- @Override
- public String getEpisodeTitle() {
- return mEpisodeTitle;
- }
+ public abstract long getDataBytes();
@Nullable
public String getEpisodeDisplayNumber(Context context) {
- if (!TextUtils.isEmpty(mEpisodeNumber)) {
- if (TextUtils.equals(mSeasonNumber, "0")) {
+ if (!TextUtils.isEmpty(getEpisodeNumber())) {
+ if (TextUtils.equals(getSeasonNumber(), "0")) {
// Do not show "S0: ".
- return String.format(
- context.getResources()
- .getString(R.string.display_episode_number_format_no_season_number),
- mEpisodeNumber);
+ return context.getResources()
+ .getString(
+ R.string.display_episode_number_format_no_season_number,
+ getEpisodeNumber());
} else {
- return String.format(
- context.getResources().getString(R.string.display_episode_number_format),
- mSeasonNumber,
- mEpisodeNumber);
+ return context.getResources()
+ .getString(
+ R.string.display_episode_number_format,
+ getSeasonNumber(),
+ getEpisodeNumber());
}
}
return null;
}
- public long getExpireTimeUtcMillis() {
- return mExpireTimeUtcMillis;
- }
-
- public long getId() {
- return mId;
- }
-
- public String getPackageName() {
- return mPackageName;
- }
-
- public String getInputId() {
- return mInputId;
- }
-
- public int getInternalProviderFlag1() {
- return mInternalProviderFlag1;
- }
-
- public int getInternalProviderFlag2() {
- return mInternalProviderFlag2;
- }
-
- public int getInternalProviderFlag3() {
- return mInternalProviderFlag3;
- }
-
- public int getInternalProviderFlag4() {
- return mInternalProviderFlag4;
- }
-
- @Override
- public String getDescription() {
- return mShortDescription;
- }
+ public abstract long getExpireTimeUtcMillis();
- @Override
- public String getLongDescription() {
- return mLongDescription;
- }
+ public abstract String getPackageName();
- @Override
- public String getPosterArtUri() {
- return mPosterArtUri;
- }
+ public abstract String getInputId();
@Override
public boolean isValid() {
return true;
}
- public boolean isSearchable() {
- return mSearchable;
- }
-
- @Override
- public String getSeriesId() {
- return mSeriesId;
- }
-
- @Override
- public String getSeasonNumber() {
- return mSeasonNumber;
+ public boolean isVisible() {
+ switch (getState()) {
+ case NOT_SET:
+ case FINISHED:
+ return true;
+ default:
+ return false;
+ }
}
- public String getSeasonTitle() {
- return mSeasonTitle;
+ public boolean isPartial() {
+ return getState() == RecordedProgramState.PARTIAL;
}
- @Override
- public long getStartTimeUtcMillis() {
- return mStartTimeUtcMillis;
- }
+ public abstract boolean isSearchable();
- @Override
- public String getThumbnailUri() {
- return mThumbnailUri;
- }
+ public abstract String getSeasonTitle();
- @Override
- public String getTitle() {
- return mTitle;
- }
+ public abstract RecordedProgramState getState();
public Uri getUri() {
- return ContentUris.withAppendedId(RecordedPrograms.CONTENT_URI, mId);
+ return ContentUris.withAppendedId(RecordedPrograms.CONTENT_URI, getId());
}
- public int getVersionNumber() {
- return mVersionNumber;
- }
+ public abstract int getVersionNumber();
- public int getVideoHeight() {
- return mVideoHeight;
- }
+ public abstract int getVideoHeight();
- public int getVideoWidth() {
- return mVideoWidth;
- }
+ public abstract int getVideoWidth();
/** Checks whether the recording has been clipped or not. */
public boolean isClipped() {
- return mEndTimeUtcMillis - mStartTimeUtcMillis - mDurationMillis > CLIPPED_THRESHOLD_MS;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- RecordedProgram that = (RecordedProgram) o;
- return Objects.equals(mId, that.mId)
- && Objects.equals(mChannelId, that.mChannelId)
- && Objects.equals(mSeriesId, that.mSeriesId)
- && Objects.equals(mSeasonNumber, that.mSeasonNumber)
- && Objects.equals(mSeasonTitle, that.mSeasonTitle)
- && Objects.equals(mEpisodeNumber, that.mEpisodeNumber)
- && Objects.equals(mStartTimeUtcMillis, that.mStartTimeUtcMillis)
- && Objects.equals(mEndTimeUtcMillis, that.mEndTimeUtcMillis)
- && Objects.equals(mVideoWidth, that.mVideoWidth)
- && Objects.equals(mVideoHeight, that.mVideoHeight)
- && Objects.equals(mSearchable, that.mSearchable)
- && Objects.equals(mDataBytes, that.mDataBytes)
- && Objects.equals(mDurationMillis, that.mDurationMillis)
- && Objects.equals(mExpireTimeUtcMillis, that.mExpireTimeUtcMillis)
- && Objects.equals(mInternalProviderFlag1, that.mInternalProviderFlag1)
- && Objects.equals(mInternalProviderFlag2, that.mInternalProviderFlag2)
- && Objects.equals(mInternalProviderFlag3, that.mInternalProviderFlag3)
- && Objects.equals(mInternalProviderFlag4, that.mInternalProviderFlag4)
- && Objects.equals(mVersionNumber, that.mVersionNumber)
- && Objects.equals(mTitle, that.mTitle)
- && Objects.equals(mEpisodeTitle, that.mEpisodeTitle)
- && Arrays.equals(mBroadcastGenres, that.mBroadcastGenres)
- && Arrays.equals(mCanonicalGenres, that.mCanonicalGenres)
- && Objects.equals(mShortDescription, that.mShortDescription)
- && Objects.equals(mLongDescription, that.mLongDescription)
- && Objects.equals(mAudioLanguage, that.mAudioLanguage)
- && Arrays.equals(mContentRatings, that.mContentRatings)
- && Objects.equals(mPosterArtUri, that.mPosterArtUri)
- && Objects.equals(mThumbnailUri, that.mThumbnailUri);
+ return getEndTimeUtcMillis() - getStartTimeUtcMillis() - getDurationMillis()
+ > CLIPPED_THRESHOLD_MS;
}
- /** Hashes based on the ID. */
- @Override
- public int hashCode() {
- return Objects.hash(mId);
- }
+ public abstract Builder toBuilder();
- @Override
- public String toString() {
- return "RecordedProgram"
- + "["
- + mId
- + "]{ mPackageName="
- + mPackageName
- + ", mInputId='"
- + mInputId
- + '\''
- + ", mChannelId='"
- + mChannelId
- + '\''
- + ", mTitle='"
- + mTitle
- + '\''
- + ", mSeriesId='"
- + mSeriesId
- + '\''
- + ", mEpisodeNumber="
- + mEpisodeNumber
- + ", mEpisodeTitle='"
- + mEpisodeTitle
- + '\''
- + ", mStartTimeUtcMillis="
- + mStartTimeUtcMillis
- + ", mEndTimeUtcMillis="
- + mEndTimeUtcMillis
- + ", mBroadcastGenres="
- + (mBroadcastGenres != null ? Arrays.toString(mBroadcastGenres) : "null")
- + ", mCanonicalGenres="
- + (mCanonicalGenres != null ? Arrays.toString(mCanonicalGenres) : "null")
- + ", mShortDescription='"
- + mShortDescription
- + '\''
- + ", mLongDescription='"
- + mLongDescription
- + '\''
- + ", mVideoHeight="
- + mVideoHeight
- + ", mVideoWidth="
- + mVideoWidth
- + ", mAudioLanguage='"
- + mAudioLanguage
- + '\''
- + ", mContentRatings='"
- + TvContentRatingCache.contentRatingsToString(mContentRatings)
- + '\''
- + ", mPosterArtUri="
- + mPosterArtUri
- + ", mThumbnailUri="
- + mThumbnailUri
- + ", mSearchable="
- + mSearchable
- + ", mDataUri="
- + mDataUri
- + ", mDataBytes="
- + mDataBytes
- + ", mDurationMillis="
- + mDurationMillis
- + ", mExpireTimeUtcMillis="
- + mExpireTimeUtcMillis
- + ", mInternalProviderFlag1="
- + mInternalProviderFlag1
- + ", mInternalProviderFlag2="
- + mInternalProviderFlag2
- + ", mInternalProviderFlag3="
- + mInternalProviderFlag3
- + ", mInternalProviderFlag4="
- + mInternalProviderFlag4
- + ", mSeasonNumber="
- + mSeasonNumber
- + ", mSeasonTitle="
- + mSeasonTitle
- + ", mVersionNumber="
- + mVersionNumber
- + '}';
+ @CheckResult
+ public RecordedProgram withId(long id) {
+ return toBuilder().setId(id).build();
}
@Nullable
@@ -925,8 +466,8 @@ public class RecordedProgram extends BaseProgram {
}
@Nullable
- private static String safeEncode(@Nullable String[] genres) {
- return genres == null ? null : TvContract.Programs.Genres.encode(genres);
+ private static String safeEncode(@Nullable ImmutableList<String> genres) {
+ return genres == null ? null : Genres.encode(genres.toArray(new String[0]));
}
/** Returns an array containing all of the elements in the list. */
diff --git a/src/com/android/tv/dvr/data/ScheduledRecording.java b/src/com/android/tv/dvr/data/ScheduledRecording.java
index 7c2d12d9..ba6d3cf9 100644
--- a/src/com/android/tv/dvr/data/ScheduledRecording.java
+++ b/src/com/android/tv/dvr/data/ScheduledRecording.java
@@ -56,39 +56,22 @@ public final class ScheduledRecording implements Parcelable {
/** Compares the start time in ascending order. */
public static final Comparator<ScheduledRecording> START_TIME_COMPARATOR =
- new Comparator<ScheduledRecording>() {
- @Override
- public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
- return Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs);
- }
- };
+ (ScheduledRecording lhs, ScheduledRecording rhs) ->
+ Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs);
/** Compares the end time in ascending order. */
public static final Comparator<ScheduledRecording> END_TIME_COMPARATOR =
- new Comparator<ScheduledRecording>() {
- @Override
- public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
- return Long.compare(lhs.mEndTimeMs, rhs.mEndTimeMs);
- }
- };
+ (ScheduledRecording lhs, ScheduledRecording rhs) ->
+ Long.compare(lhs.mEndTimeMs, rhs.mEndTimeMs);
/** Compares ID in ascending order. The schedule with the larger ID was created later. */
public static final Comparator<ScheduledRecording> ID_COMPARATOR =
- new Comparator<ScheduledRecording>() {
- @Override
- public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
- return Long.compare(lhs.mId, rhs.mId);
- }
- };
+ (ScheduledRecording lhs, ScheduledRecording rhs) -> Long.compare(lhs.mId, rhs.mId);
/** Compares the priority in ascending order. */
public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR =
- new Comparator<ScheduledRecording>() {
- @Override
- public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
- return Long.compare(lhs.mPriority, rhs.mPriority);
- }
- };
+ (ScheduledRecording lhs, ScheduledRecording rhs) ->
+ Long.compare(lhs.mPriority, rhs.mPriority);
/**
* Compares start time in ascending order and then priority in descending order and then ID in
@@ -359,15 +342,22 @@ public final class ScheduledRecording implements Parcelable {
})
public @interface RecordingFailedReason {}
+ // next number for failed reason: 11
public static final int FAILED_REASON_OTHER = 0;
- public static final int FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED = 1;
public static final int FAILED_REASON_NOT_FINISHED = 2;
public static final int FAILED_REASON_SCHEDULER_STOPPED = 3;
public static final int FAILED_REASON_INVALID_CHANNEL = 4;
public static final int FAILED_REASON_MESSAGE_NOT_SENT = 5;
public static final int FAILED_REASON_CONNECTION_FAILED = 6;
+
+ // for the following reasons, show advice to users
+ // TODO(b/72638597): add failure condition of "weak signal"
+
+ // failed reason is FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED when tuner or external
+ // storage is disconnected
+ public static final int FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED = 1;
+ // failed reason is FAILED_REASON_RESOURCE_BUSY when antenna is disconnected or signal is weak
public static final int FAILED_REASON_RESOURCE_BUSY = 7;
- // For the following reasons, show advice to users
public static final int FAILED_REASON_INPUT_UNAVAILABLE = 8;
public static final int FAILED_REASON_INPUT_DVR_UNSUPPORTED = 9;
public static final int FAILED_REASON_INSUFFICIENT_SPACE = 10;
@@ -679,7 +669,8 @@ public final class ScheduledRecording implements Parcelable {
}
/** Returns the failed reason of the {@link ScheduledRecording}. */
- @Nullable @RecordingFailedReason
+ @Nullable
+ @RecordingFailedReason
public Integer getFailedReason() {
return mFailedReason;
}
@@ -812,10 +803,7 @@ public final class ScheduledRecording implements Parcelable {
}
}
- /**
- * Converts a string to a failed reason integer, defaulting to {@link
- * #FAILED_REASON_OTHER}.
- */
+ /** Converts a string to a failed reason integer, defaulting to {@link #FAILED_REASON_OTHER}. */
private static Integer recordingFailedReason(String reason) {
if (TextUtils.isEmpty(reason)) {
return null;
@@ -985,6 +973,11 @@ public final class ScheduledRecording implements Parcelable {
return mState == STATE_RECORDING_FINISHED;
}
+ /** Returns {@code true} if the recording is failed, otherwise @{code false}. */
+ public boolean isFailed() {
+ return mState == STATE_RECORDING_FAILED;
+ }
+
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ScheduledRecording)) {
diff --git a/src/com/android/tv/dvr/data/SeriesRecording.java b/src/com/android/tv/dvr/data/SeriesRecording.java
index 96b3425a..6cb0e836 100644
--- a/src/com/android/tv/dvr/data/SeriesRecording.java
+++ b/src/com/android/tv/dvr/data/SeriesRecording.java
@@ -49,9 +49,8 @@ public class SeriesRecording implements Parcelable {
@Retention(RetentionPolicy.SOURCE)
@IntDef(
- flag = true,
- value = {OPTION_CHANNEL_ONE, OPTION_CHANNEL_ALL}
- )
+ flag = true,
+ value = {OPTION_CHANNEL_ONE, OPTION_CHANNEL_ALL})
public @interface ChannelOption {}
/** An option which indicates that the episodes in one channel are recorded. */
public static final int OPTION_CHANNEL_ONE = 0;
@@ -60,9 +59,8 @@ public class SeriesRecording implements Parcelable {
@Retention(RetentionPolicy.SOURCE)
@IntDef(
- flag = true,
- value = {STATE_SERIES_NORMAL, STATE_SERIES_STOPPED}
- )
+ flag = true,
+ value = {STATE_SERIES_NORMAL, STATE_SERIES_STOPPED})
public @interface SeriesState {}
/** The state indicates that the series recording is a normal one. */
@@ -73,26 +71,18 @@ public class SeriesRecording implements Parcelable {
/** Compare priority in descending order. */
public static final Comparator<SeriesRecording> PRIORITY_COMPARATOR =
- new Comparator<SeriesRecording>() {
- @Override
- public int compare(SeriesRecording lhs, SeriesRecording rhs) {
- int value = Long.compare(rhs.mPriority, lhs.mPriority);
- if (value == 0) {
- // New recording has the higher priority.
- value = Long.compare(rhs.mId, lhs.mId);
- }
- return value;
+ (SeriesRecording lhs, SeriesRecording rhs) -> {
+ int value = Long.compare(rhs.mPriority, lhs.mPriority);
+ if (value == 0) {
+ // New recording has the higher priority.
+ value = Long.compare(rhs.mId, lhs.mId);
}
+ return value;
};
/** Compare ID in ascending order. */
public static final Comparator<SeriesRecording> ID_COMPARATOR =
- new Comparator<SeriesRecording>() {
- @Override
- public int compare(SeriesRecording lhs, SeriesRecording rhs) {
- return Long.compare(lhs.mId, rhs.mId);
- }
- };
+ (SeriesRecording lhs, SeriesRecording rhs) -> Long.compare(lhs.mId, rhs.mId);
/**
* Creates a new Builder with the values set from the series information of {@link BaseProgram}.
diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
index 41e5a66a..ebf133db 100644
--- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
+++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
@@ -79,6 +79,8 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
+ " TEXT,"
+ Schedules.COLUMN_STATE
+ " TEXT NOT NULL,"
+ + Schedules.COLUMN_FAILED_REASON
+ + " TEXT,"
+ Schedules.COLUMN_SERIES_RECORDING_ID
+ " INTEGER,"
+ "FOREIGN KEY("
@@ -261,6 +263,7 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_SERIES_RECORDINGS);
db.execSQL(SQL_DROP_SERIES_RECORDINGS);
onCreate(db);
+ return;
}
if (oldVersion < 18) {
db.execSQL("ALTER TABLE " + Schedules.TABLE_NAME + " ADD COLUMN "
diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/DvrDbFuture.java
index 7d2af9c3..ae8c480b 100644
--- a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
+++ b/src/com/android/tv/dvr/provider/DvrDbFuture.java
@@ -18,109 +18,111 @@ package com.android.tv.dvr.provider;
import android.content.Context;
import android.database.Cursor;
-import android.os.AsyncTask;
import android.support.annotation.Nullable;
+import android.util.Log;
import com.android.tv.common.concurrent.NamedThreadFactory;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.provider.DvrContract.Schedules;
import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
+import com.android.tv.util.MainThreadExecutor;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-/** {@link AsyncTask} that defaults to executing on its own single threaded Executor Service. */
-public abstract class AsyncDvrDbTask<Params, Progress, Result>
- extends AsyncTask<Params, Progress, Result> {
+/** {@link DvrDbFuture} that defaults to executing on its own single threaded Executor Service. */
+public abstract class DvrDbFuture<ParamsT, ResultT> {
private static final NamedThreadFactory THREAD_FACTORY =
- new NamedThreadFactory(AsyncDvrDbTask.class.getSimpleName());
- private static final ExecutorService DB_EXECUTOR =
- Executors.newSingleThreadExecutor(THREAD_FACTORY);
+ new NamedThreadFactory(DvrDbFuture.class.getSimpleName());
+ private static final ListeningExecutorService DB_EXECUTOR =
+ MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(THREAD_FACTORY));
private static DvrDatabaseHelper sDbHelper;
-
- private static synchronized DvrDatabaseHelper initializeDbHelper(Context context) {
- if (sDbHelper == null) {
- sDbHelper = new DvrDatabaseHelper(context.getApplicationContext());
- }
- return sDbHelper;
- }
+ private ListenableFuture<ResultT> mFuture;
final Context mContext;
- private AsyncDvrDbTask(Context context) {
+ private DvrDbFuture(Context context) {
mContext = context;
}
- /** Execute the task on the {@link #DB_EXECUTOR} thread. */
+ /** Execute the task on the {@link #DB_EXECUTOR} thread and return Future*/
@SafeVarargs
- public final void executeOnDbThread(Params... params) {
- executeOnExecutor(DB_EXECUTOR, params);
- }
-
- @Override
- protected final Result doInBackground(Params... params) {
- initializeDbHelper(mContext);
- return doInDvrBackground(params);
+ public final ListenableFuture<ResultT> executeOnDbThread(
+ FutureCallback<ResultT> callback, ParamsT... params) {
+ if (sDbHelper == null) {
+ sDbHelper = new DvrDatabaseHelper(mContext.getApplicationContext());
+ }
+ mFuture = DB_EXECUTOR.submit(() -> dbHelperInBackground(params));
+ Futures.addCallback(mFuture, callback, MainThreadExecutor.getInstance());
+ return mFuture;
}
- /** Executes in the background after {@link #initializeDbHelper(Context)} */
+ /** Executes in the background after initializing DbHelper} */
@Nullable
- protected abstract Result doInDvrBackground(Params... params);
+ protected abstract ResultT dbHelperInBackground(ParamsT... params);
+
+ public final boolean isCancelled() {
+ return mFuture.isCancelled();
+ }
/** Inserts schedules. */
- public static class AsyncAddScheduleTask
- extends AsyncDvrDbTask<ScheduledRecording, Void, Void> {
- public AsyncAddScheduleTask(Context context) {
+ public static class AddScheduleFuture
+ extends DvrDbFuture<ScheduledRecording, Void> {
+ public AddScheduleFuture(Context context) {
super(context);
}
@Override
- protected final Void doInDvrBackground(ScheduledRecording... params) {
+ protected final Void dbHelperInBackground(ScheduledRecording... params) {
sDbHelper.insertSchedules(params);
return null;
}
}
/** Update schedules. */
- public static class AsyncUpdateScheduleTask
- extends AsyncDvrDbTask<ScheduledRecording, Void, Void> {
- public AsyncUpdateScheduleTask(Context context) {
+ public static class UpdateScheduleFuture
+ extends DvrDbFuture<ScheduledRecording, Void> {
+ public UpdateScheduleFuture(Context context) {
super(context);
}
@Override
- protected final Void doInDvrBackground(ScheduledRecording... params) {
+ protected final Void dbHelperInBackground(ScheduledRecording... params) {
sDbHelper.updateSchedules(params);
return null;
}
}
/** Delete schedules. */
- public static class AsyncDeleteScheduleTask
- extends AsyncDvrDbTask<ScheduledRecording, Void, Void> {
- public AsyncDeleteScheduleTask(Context context) {
+ public static class DeleteScheduleFuture
+ extends DvrDbFuture<ScheduledRecording, Void> {
+ public DeleteScheduleFuture(Context context) {
super(context);
}
@Override
- protected final Void doInDvrBackground(ScheduledRecording... params) {
+ protected final Void dbHelperInBackground(ScheduledRecording... params) {
sDbHelper.deleteSchedules(params);
return null;
}
}
/** Returns all {@link ScheduledRecording}s. */
- public abstract static class AsyncDvrQueryScheduleTask
- extends AsyncDvrDbTask<Void, Void, List<ScheduledRecording>> {
- public AsyncDvrQueryScheduleTask(Context context) {
+ public static class DvrQueryScheduleFuture
+ extends DvrDbFuture<Void, List<ScheduledRecording>> {
+ public DvrQueryScheduleFuture(Context context) {
super(context);
}
@Override
@Nullable
- protected final List<ScheduledRecording> doInDvrBackground(Void... params) {
+ protected final List<ScheduledRecording> dbHelperInBackground(Void... params) {
if (isCancelled()) {
return null;
}
@@ -135,57 +137,59 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result>
}
/** Inserts series recordings. */
- public static class AsyncAddSeriesRecordingTask
- extends AsyncDvrDbTask<SeriesRecording, Void, Void> {
- public AsyncAddSeriesRecordingTask(Context context) {
+ public static class AddSeriesRecordingFuture
+ extends DvrDbFuture<SeriesRecording, Void> {
+ public AddSeriesRecordingFuture(Context context) {
super(context);
}
@Override
- protected final Void doInDvrBackground(SeriesRecording... params) {
+ protected final Void dbHelperInBackground(SeriesRecording... params) {
sDbHelper.insertSeriesRecordings(params);
return null;
}
}
/** Update series recordings. */
- public static class AsyncUpdateSeriesRecordingTask
- extends AsyncDvrDbTask<SeriesRecording, Void, Void> {
- public AsyncUpdateSeriesRecordingTask(Context context) {
+ public static class UpdateSeriesRecordingFuture
+ extends DvrDbFuture<SeriesRecording, Void> {
+ public UpdateSeriesRecordingFuture(Context context) {
super(context);
}
@Override
- protected final Void doInDvrBackground(SeriesRecording... params) {
+ protected final Void dbHelperInBackground(SeriesRecording... params) {
sDbHelper.updateSeriesRecordings(params);
return null;
}
}
/** Delete series recordings. */
- public static class AsyncDeleteSeriesRecordingTask
- extends AsyncDvrDbTask<SeriesRecording, Void, Void> {
- public AsyncDeleteSeriesRecordingTask(Context context) {
+ public static class DeleteSeriesRecordingFuture
+ extends DvrDbFuture<SeriesRecording, Void> {
+ public DeleteSeriesRecordingFuture(Context context) {
super(context);
}
@Override
- protected final Void doInDvrBackground(SeriesRecording... params) {
+ protected final Void dbHelperInBackground(SeriesRecording... params) {
sDbHelper.deleteSeriesRecordings(params);
return null;
}
}
/** Returns all {@link SeriesRecording}s. */
- public abstract static class AsyncDvrQuerySeriesRecordingTask
- extends AsyncDvrDbTask<Void, Void, List<SeriesRecording>> {
- public AsyncDvrQuerySeriesRecordingTask(Context context) {
+ public static class DvrQuerySeriesRecordingFuture
+ extends DvrDbFuture<Void, List<SeriesRecording>> {
+ private static final String TAG = "DvrQuerySeriesRecording";
+
+ public DvrQuerySeriesRecordingFuture(Context context) {
super(context);
}
@Override
@Nullable
- protected final List<SeriesRecording> doInDvrBackground(Void... params) {
+ protected final List<SeriesRecording> dbHelperInBackground(Void... params) {
if (isCancelled()) {
return null;
}
@@ -195,6 +199,8 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result>
while (c.moveToNext() && !isCancelled()) {
scheduledRecordings.add(SeriesRecording.fromCursor(c));
}
+ } catch (Exception e) {
+ Log.w(TAG, "Can't query dvr series recording data", e);
}
return scheduledRecordings;
}
diff --git a/src/com/android/tv/dvr/provider/DvrDbSync.java b/src/com/android/tv/dvr/provider/DvrDbSync.java
index 42bc8bcc..7658ca45 100644
--- a/src/com/android/tv/dvr/provider/DvrDbSync.java
+++ b/src/com/android/tv/dvr/provider/DvrDbSync.java
@@ -277,7 +277,6 @@ public class DvrDbSync {
}
}
} else {
- long currentTimeMs = System.currentTimeMillis();
ScheduledRecording.Builder builder =
ScheduledRecording.buildFrom(schedule)
.setEndTimeMs(program.getEndTimeUtcMillis())
@@ -361,7 +360,7 @@ public class DvrDbSync {
private final long mProgramId;
QueryProgramTask(long programId) {
- super(mDbExecutor, mContext.getContentResolver(), programId);
+ super(mDbExecutor, mContext, programId);
mProgramId = programId;
}
diff --git a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
index b7d9f3b3..02e197f1 100644
--- a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
+++ b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
@@ -186,7 +186,7 @@ public abstract class EpisodicProgramLoadTask {
SqlParams sqlParams = createSqlParams();
return new AsyncProgramQueryTask(
TvSingletons.getSingletons(mContext).getDbExecutor(),
- mContext.getContentResolver(),
+ mContext,
sqlParams.uri,
sqlParams.selection,
sqlParams.selectionArgs,
@@ -284,7 +284,7 @@ public abstract class EpisodicProgramLoadTask {
@Override
@WorkerThread
- public boolean filter(Cursor c) {
+ public boolean apply(Cursor c) {
if (!mLoadDisallowedProgram
&& mDisallowedProgramIds.contains(c.getLong(PROGRAM_ID_INDEX))) {
return false;
@@ -318,10 +318,10 @@ public abstract class EpisodicProgramLoadTask {
}
@Override
- public boolean filter(Cursor c) {
+ public boolean apply(Cursor c) {
return (mLoadCurrentProgram || c.getLong(START_TIME_INDEX) > System.currentTimeMillis())
&& c.getInt(RECORDING_PROHIBITED_INDEX) != 0
- && super.filter(c);
+ && super.apply(c);
}
}
diff --git a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
index 1021b2bc..7d9f7fe2 100644
--- a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
+++ b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
@@ -279,7 +279,8 @@ public class InputTaskScheduler {
if (schedule.getEndTimeMs() - currentTimeMs
<= MIN_REMAIN_DURATION_PERCENT * schedule.getDuration()) {
Log.e(TAG, "Error! Program ended before recording started:" + schedule);
- fail(schedule,
+ fail(
+ schedule,
ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED);
iter.remove();
}
@@ -394,19 +395,16 @@ public class InputTaskScheduler {
private void fail(ScheduledRecording schedule, int reason) {
// It's called when the scheduling has been failed without creating RecordingTask.
runOnMainHandler(
- new Runnable() {
- @Override
- public void run() {
- ScheduledRecording scheduleInManager =
- mDataManager.getScheduledRecording(schedule.getId());
- if (scheduleInManager != null) {
- // The schedule should be updated based on the object from DataManager
- // in case when it has been updated.
- mDataManager.changeState(
- scheduleInManager,
- ScheduledRecording.STATE_RECORDING_FAILED,
- reason);
- }
+ () -> {
+ ScheduledRecording scheduleInManager =
+ mDataManager.getScheduledRecording(schedule.getId());
+ if (scheduleInManager != null) {
+ // The schedule should be updated based on the object from DataManager
+ // in case when it has been updated.
+ mDataManager.changeState(
+ scheduleInManager,
+ ScheduledRecording.STATE_RECORDING_FAILED,
+ reason);
}
});
}
diff --git a/src/com/android/tv/dvr/recorder/RecordingTask.java b/src/com/android/tv/dvr/recorder/RecordingTask.java
index 07a29e51..98f668a0 100644
--- a/src/com/android/tv/dvr/recorder/RecordingTask.java
+++ b/src/com/android/tv/dvr/recorder/RecordingTask.java
@@ -17,10 +17,11 @@
package com.android.tv.dvr.recorder;
import android.annotation.TargetApi;
+import android.content.ContentUris;
+import android.content.ContentValues;
import android.content.Context;
import android.media.tv.TvContract;
import android.media.tv.TvInputManager;
-import android.media.tv.TvRecordingClient.RecordingCallback;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
@@ -36,6 +37,7 @@ import com.android.tv.InputSessionManager.RecordingSession;
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.compat.TvRecordingClientCompat.RecordingCallbackCompat;
import com.android.tv.common.util.Clock;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.data.api.Channel;
@@ -55,7 +57,7 @@ import java.util.concurrent.TimeUnit;
*/
@WorkerThread
@TargetApi(Build.VERSION_CODES.N)
-public class RecordingTask extends RecordingCallback
+public class RecordingTask extends RecordingCallbackCompat
implements Handler.Callback, DvrManager.Listener {
private static final String TAG = "RecordingTask";
private static final boolean DEBUG = false;
@@ -223,6 +225,14 @@ public class RecordingTask extends RecordingCallback
}
@Override
+ public void onRecordingStarted(String inputId, String recUri) {
+ if (DEBUG) {
+ Log.d(TAG, "onRecordingStart");
+ }
+ addRecordedProgramId(recUri);
+ }
+
+ @Override
public void onRecordingStopped(Uri recordedProgramUri) {
Log.i(TAG, "Recording Stopped: " + mScheduledRecording);
Log.i(TAG, "Recording Stopped: stored as " + recordedProgramUri);
@@ -340,10 +350,8 @@ public class RecordingTask extends RecordingCallback
}
private void failAndQuit(Integer reason) {
- if (DEBUG) Log.d(TAG, "failAndQuit");
- updateRecordingState(
- ScheduledRecording.STATE_RECORDING_FAILED,
- reason);
+ Log.w(TAG, "Recording " + mScheduledRecording + " failed with code " + reason);
+ updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED, reason);
mState = State.ERROR;
sendRemove();
}
@@ -450,6 +458,7 @@ public class RecordingTask extends RecordingCallback
private void updateRecordingState(@ScheduledRecording.RecordingState int state) {
updateRecordingState(state, null);
}
+
private void updateRecordingState(
@ScheduledRecording.RecordingState int state, @Nullable Integer reason) {
if (DEBUG) {
@@ -471,9 +480,7 @@ public class RecordingTask extends RecordingCallback
// has been updated. mScheduledRecording will be updated from
// onScheduledRecordingStateChanged.
ScheduledRecording.Builder builder =
- ScheduledRecording
- .buildFrom(schedule)
- .setState(state);
+ ScheduledRecording.buildFrom(schedule).setState(state);
if (state == ScheduledRecording.STATE_RECORDING_FAILED
&& reason != null) {
builder.setFailedReason(reason);
@@ -484,6 +491,43 @@ public class RecordingTask extends RecordingCallback
});
}
+ private void addRecordedProgramId(String recordedProgramUri) {
+ if (DEBUG) {
+ Log.d(TAG, "Adding Recorded Program Id to " + mScheduledRecording);
+ }
+ mRecordedProgramUri = Uri.parse(recordedProgramUri);
+ long id = ContentUris.parseId(mRecordedProgramUri);
+ mScheduledRecording =
+ ScheduledRecording.buildFrom(mScheduledRecording).setRecordedProgramId(id).build();
+ ContentValues values = new ContentValues();
+ values.put(
+ TvContract.RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
+ mScheduledRecording.getEndTimeMs() - mScheduledRecording.getStartTimeMs());
+ values.put(
+ TvContract.RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS,
+ mScheduledRecording.getEndTimeMs());
+ mContext.getContentResolver().update(mRecordedProgramUri, values, null, null);
+ runOnMainThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ ScheduledRecording schedule =
+ mDataManager.getScheduledRecording(mScheduledRecording.getId());
+ if (schedule == null) {
+ // Schedule has been deleted. Delete the recorded program.
+ removeRecordedProgram();
+ } else {
+ // Update the state based on the object in DataManager in case when it
+ // has been updated. mScheduledRecording will be updated from
+ // onScheduledRecordingStateChanged.
+ ScheduledRecording.Builder builder =
+ ScheduledRecording.buildFrom(schedule).setRecordedProgramId(id);
+ mDataManager.updateScheduledRecording(builder.build());
+ }
+ }
+ });
+ }
+
@Override
public void onStopRecordingRequested(ScheduledRecording recording) {
if (recording.getId() != mScheduledRecording.getId()) {
@@ -553,7 +597,7 @@ public class RecordingTask extends RecordingCallback
@Override
public void run() {
if (mRecordedProgramUri != null) {
- mDvrManager.removeRecordedProgram(mRecordedProgramUri);
+ mDvrManager.removeRecordedProgram(mRecordedProgramUri, true);
}
}
});
diff --git a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
index 4f7a789b..696038cf 100644
--- a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
+++ b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
@@ -29,7 +29,6 @@ import android.util.Log;
import android.util.LongSparseArray;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.experiments.Experiments;
import com.android.tv.common.util.CollectionUtils;
import com.android.tv.common.util.SharedPreferencesUtils;
import com.android.tv.data.Program;
@@ -48,7 +47,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
-import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -261,14 +259,11 @@ public class SeriesRecordingScheduler {
}
private void executeFetchSeriesInfoTask(SeriesRecording seriesRecording) {
- if (Experiments.CLOUD_EPG.get()) {
- FetchSeriesInfoTask task =
- new FetchSeriesInfoTask(
- seriesRecording,
- TvSingletons.getSingletons(mContext).providesEpgReader());
- task.execute();
- mFetchSeriesInfoTasks.put(seriesRecording.getId(), task);
- }
+ FetchSeriesInfoTask task =
+ new FetchSeriesInfoTask(
+ seriesRecording, TvSingletons.getSingletons(mContext).providesEpgReader());
+ task.execute();
+ mFetchSeriesInfoTasks.put(seriesRecording.getId(), task);
}
/** Pauses the updates of the series recordings. */
@@ -442,21 +437,18 @@ public class SeriesRecordingScheduler {
List<Program> programsForEpisode = entry.getValue();
Collections.sort(
programsForEpisode,
- new Comparator<Program>() {
- @Override
- public int compare(Program lhs, Program rhs) {
- // Place the existing schedule first.
- boolean lhsScheduled = isProgramScheduled(dataManager, lhs);
- boolean rhsScheduled = isProgramScheduled(dataManager, rhs);
- if (lhsScheduled && !rhsScheduled) {
- return -1;
- }
- if (!lhsScheduled && rhsScheduled) {
- return 1;
- }
- // Sort by the start time in ascending order.
- return lhs.compareTo(rhs);
+ (Program lhs, Program rhs) -> {
+ // Place the existing schedule first.
+ boolean lhsScheduled = isProgramScheduled(dataManager, lhs);
+ boolean rhsScheduled = isProgramScheduled(dataManager, rhs);
+ if (lhsScheduled && !rhsScheduled) {
+ return -1;
+ }
+ if (!lhsScheduled && rhsScheduled) {
+ return 1;
}
+ // Sort by the start time in ascending order.
+ return lhs.compareTo(rhs);
});
boolean added = false;
// Add all the scheduled programs
diff --git a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java
index 32679421..9cd91a64 100644
--- a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java
+++ b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java
@@ -27,13 +27,15 @@ import android.view.View;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import com.android.tv.R;
+import com.android.tv.ui.DetailsActivity;
+
import java.util.Map;
/**
* TODO: Remove this class once b/32405620 is fixed. This class is for the workaround of b/32405620
* and only for the shared element transition between {@link
* com.android.tv.dvr.ui.browse.RecordingCardView} and {@link
- * com.android.tv.dvr.ui.browse.DvrDetailsActivity}.
+ * DetailsActivity}.
*/
public class ChangeImageTransformWithScaledParent extends ChangeImageTransform {
private static final String PROPNAME_MATRIX = "android:changeImageTransform:matrix";
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
index fce94230..5e3caa9c 100644
--- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
@@ -71,7 +71,7 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment {
public Guidance onCreateGuidance(Bundle savedInstanceState) {
String title = getString(R.string.dvr_already_recorded_dialog_title);
String description = getString(R.string.dvr_already_recorded_dialog_description);
- Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null);
+ Drawable image = getResources().getDrawable(R.drawable.quantum_ic_warning_white_96, null);
return new Guidance(title, description, null, image);
}
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
index 456ad830..a6bbe137 100644
--- a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
@@ -78,7 +78,7 @@ public class DvrAlreadyScheduledFragment extends DvrGuidedStepFragment {
getContext(),
mDuplicate.getStartTimeMs(),
DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE));
- Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null);
+ Drawable image = getResources().getDrawable(R.drawable.quantum_ic_warning_white_96, null);
return new Guidance(title, description, null, image);
}
diff --git a/src/com/android/tv/dvr/ui/DvrConflictFragment.java b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
index 65759555..649cc89a 100644
--- a/src/com/android/tv/dvr/ui/DvrConflictFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
@@ -205,7 +205,7 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment {
if (description == null) {
dismissDialog();
}
- Drawable icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null);
+ Drawable icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null);
return new Guidance(title, descriptionPrefix + " " + description, null, icon);
}
@@ -265,7 +265,7 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment {
if (description == null) {
dismissDialog();
}
- Drawable icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null);
+ Drawable icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null);
return new Guidance(title, descriptionPrefix + " " + description, null, icon);
}
diff --git a/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java b/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java
deleted file mode 100644
index 677a6cbb..00000000
--- a/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2018 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.os.Bundle;
-import android.support.v17.leanback.widget.GuidanceStylist;
-import android.support.v17.leanback.widget.GuidedAction;
-import com.android.tv.TvSingletons;
-import com.android.tv.data.Program;
-import com.android.tv.dvr.data.ScheduledRecording;
-import com.android.tv.util.Utils;
-import java.util.List;
-
-/**
- * A fragment which shows the formation of a program.
- */
-public class DvrFutureProgramInfoFragment extends DvrGuidedStepFragment {
- private static final long ACTION_ID_VIEW_SCHEDULE = 1;
- private ScheduledRecording mScheduledRecording;
- private Program mProgram;
-
- @Override
- public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
- long startTime = mProgram.getStartTimeUtcMillis();
- // TODO(b/71717923): use R.string when the strings are finalized
- StringBuilder description = new StringBuilder()
- .append("This program will start at ")
- .append(Utils.getDurationString(getContext(), startTime, startTime, false));
- if (mScheduledRecording != null) {
- description.append("\nThis program has been scheduled for recording.");
- }
- return new GuidanceStylist.Guidance(
- mProgram.getTitle(), description.toString(), null, null);
- }
-
- @Override
- public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- Activity activity = getActivity();
- mProgram = getArguments().getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM);
- mScheduledRecording =
- TvSingletons.getSingletons(getContext())
- .getDvrDataManager()
- .getScheduledRecordingForProgramId(mProgram.getId());
- actions.add(
- new GuidedAction.Builder(activity)
- .id(GuidedAction.ACTION_ID_OK)
- .title(android.R.string.ok)
- .build());
- if (mScheduledRecording != null) {
- actions.add(
- new GuidedAction.Builder(activity)
- .id(ACTION_ID_VIEW_SCHEDULE)
- .title("View schedules")
- .build());
- }
-
- }
-
- @Override
- public void onTrackedGuidedActionClicked(GuidedAction action) {
- if (action.getId() == ACTION_ID_VIEW_SCHEDULE) {
- DvrUiHelper.startSchedulesActivity(getContext(), mScheduledRecording);
- return;
- }
- dismissDialog();
- }
-
- @Override
- public String getTrackerPrefix() {
- return "DvrFutureProgramInfoFragment";
- }
-}
diff --git a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
index 4a713703..e6b54f67 100644
--- a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
@@ -18,17 +18,20 @@ package com.android.tv.dvr.ui;
import android.app.Activity;
import android.content.Context;
+import android.content.DialogInterface;
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.dialog.HalfSizedDialogFragment;
import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelWatchConflictFragment;
import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment;
import com.android.tv.guide.ProgramGuide;
+import com.android.tv.ui.DetailsActivity;
public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
/** Key for input ID. Type: String. */
@@ -187,11 +190,27 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
}
}
- /** A dialog fragment for {@link DvrFutureProgramInfoFragment}. */
- public static class DvrFutureProgramInfoDialogFragment extends DvrGuidedStepDialogFragment {
+ /** A dialog fragment for {@link DvrWriteStoragePermissionRationaleFragment}. */
+ public static class DvrWriteStoragePermissionRationaleDialogFragment
+ extends DvrGuidedStepDialogFragment {
@Override
- protected DvrGuidedStepFragment onCreateGuidedStepFragment() {
- return new DvrFutureProgramInfoFragment();
+ protected DvrWriteStoragePermissionRationaleFragment onCreateGuidedStepFragment() {
+ return new DvrWriteStoragePermissionRationaleFragment();
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ Activity activity = getActivity();
+ if (activity instanceof DetailsActivity) {
+ activity.requestPermissions(
+ new String[] {"android.permission.WRITE_EXTERNAL_STORAGE"},
+ DetailsActivity.REQUEST_DELETE);
+ } else if (activity instanceof DvrSeriesDeletionActivity) {
+ activity.requestPermissions(
+ new String[] {"android.permission.WRITE_EXTERNAL_STORAGE"},
+ DvrSeriesDeletionActivity.REQUEST_DELETE);
+ }
+ super.onDismiss(dialog);
}
}
}
diff --git a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
index e5f40260..02b2da1d 100644
--- a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
@@ -25,7 +25,7 @@ import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
import android.util.Log;
import com.android.tv.R;
-import com.android.tv.dvr.ui.browse.DvrDetailsActivity;
+import com.android.tv.ui.DetailsActivity;
import java.util.List;
public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment {
@@ -65,7 +65,7 @@ public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment {
@Override
public void onTrackedGuidedActionClicked(GuidedAction action) {
Activity activity = getActivity();
- if (activity instanceof DvrDetailsActivity) {
+ if (activity instanceof DetailsActivity) {
activity.finish();
} else {
dismissDialog();
diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
index 5251e140..72603d03 100644
--- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
@@ -34,7 +34,6 @@ import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment;
-import com.android.tv.util.Utils;
import java.util.Collections;
import java.util.List;
@@ -104,12 +103,7 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment {
mProgram.getEndTimeUtcMillis(),
DateUtils.FORMAT_SHOW_TIME));
} else {
- description =
- Utils.getDurationString(
- context,
- mProgram.getStartTimeUtcMillis(),
- mProgram.getEndTimeUtcMillis(),
- true);
+ description = mProgram.getDurationString(context);
}
actions.add(
new GuidedAction.Builder(context)
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
index a2ae1f97..a237f1d2 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
@@ -17,16 +17,34 @@
package com.android.tv.dvr.ui;
import android.app.Activity;
+import android.content.pm.PackageManager;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.support.v17.leanback.app.GuidedStepFragment;
+import android.util.Log;
+import android.widget.Toast;
+
import com.android.tv.R;
import com.android.tv.Starter;
+import com.android.tv.TvSingletons;
+import com.android.tv.dvr.DvrManager;
+
+import java.util.ArrayList;
+import java.util.List;
/** Activity to show details view in DVR. */
public class DvrSeriesDeletionActivity extends Activity {
+ private static final String TAG = "DvrSeriesDeletionActivity";
+
/** Name of series id added to the Intent. */
public static final String SERIES_RECORDING_ID = "series_recording_id";
+ public static final int REQUEST_DELETE = 1;
+ public static final long INVALID_SERIES_RECORDING_ID = -1;
+
+ private long mSeriesRecordingId = INVALID_SERIES_RECORDING_ID;
+ private final List<Long> mIdsToDelete = new ArrayList<>();
+
@Override
public void onCreate(Bundle savedInstanceState) {
Starter.start(this);
@@ -34,9 +52,61 @@ public class DvrSeriesDeletionActivity extends Activity {
setContentView(R.layout.activity_dvr_series_settings);
// Check savedInstanceState to prevent that activity is being showed with animation.
if (savedInstanceState == null) {
+ mSeriesRecordingId =
+ getIntent().getLongExtra(SERIES_RECORDING_ID, INVALID_SERIES_RECORDING_ID);
DvrSeriesDeletionFragment deletionFragment = new DvrSeriesDeletionFragment();
deletionFragment.setArguments(getIntent().getExtras());
GuidedStepFragment.addAsRoot(this, deletionFragment, R.id.dvr_settings_view_frame);
}
}
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ switch (requestCode) {
+ case REQUEST_DELETE:
+ // If request is cancelled, the result arrays are empty.
+ if (grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ deleteSelectedIds(true);
+ } else {
+ // NOTE: If Live TV ever supports both embedded and separate DVR inputs
+ // then we should try to do the delete regardless.
+ Log.i(
+ TAG,
+ "Write permission denied, Not trying to delete the files for series "
+ + mSeriesRecordingId);
+ deleteSelectedIds(false);
+ }
+ break;
+ default:
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
+ private void deleteSelectedIds(boolean deleteFiles) {
+ TvSingletons singletons = TvSingletons.getSingletons(this);
+ int recordingSize =
+ singletons.getDvrDataManager().getRecordedPrograms(mSeriesRecordingId).size();
+ if (!mIdsToDelete.isEmpty()) {
+ DvrManager dvrManager = singletons.getDvrManager();
+ dvrManager.removeRecordedPrograms(mIdsToDelete, deleteFiles);
+ }
+ Toast.makeText(
+ this,
+ getResources()
+ .getQuantityString(
+ R.plurals.dvr_msg_episodes_deleted,
+ mIdsToDelete.size(),
+ mIdsToDelete.size(),
+ recordingSize),
+ Toast.LENGTH_LONG)
+ .show();
+ finish();
+ }
+
+ void setIdsToDelete(List<Long> ids) {
+ mIdsToDelete.clear();
+ mIdsToDelete.addAll(ids);
+ }
}
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
index 685f0a58..ff213231 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
@@ -29,6 +29,7 @@ import android.widget.Toast;
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.util.PermissionUtils;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
@@ -53,10 +54,12 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment {
private static final long ACTION_ID_SELECT_ALL = -111;
private static final long ACTION_ID_DELETE = -112;
+ private DvrManager mDvrManager;
private DvrDataManager mDvrDataManager;
private DvrWatchedPositionManager mDvrWatchedPositionManager;
private List<RecordedProgram> mRecordings;
private final Set<Long> mWatchedRecordings = new HashSet<>();
+ private final List<Long> mIdsToDelete = new ArrayList<>();
private boolean mAllSelected;
private long mSeriesRecordingId;
private int mOneLineActionHeight;
@@ -67,9 +70,10 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment {
mSeriesRecordingId =
getArguments().getLong(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, -1);
SoftPreconditions.checkArgument(mSeriesRecordingId != -1);
- mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager();
- mDvrWatchedPositionManager =
- TvSingletons.getSingletons(context).getDvrWatchedPositionManager();
+ TvSingletons singletons = TvSingletons.getSingletons(context);
+ mDvrManager = singletons.getDvrManager();
+ mDvrDataManager = singletons.getDvrDataManager();
+ mDvrWatchedPositionManager = singletons.getDvrWatchedPositionManager();
mRecordings = mDvrDataManager.getRecordedPrograms(mSeriesRecordingId);
mOneLineActionHeight =
getResources()
@@ -158,28 +162,7 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment {
public void onGuidedActionClicked(GuidedAction action) {
long actionId = action.getId();
if (actionId == ACTION_ID_DELETE) {
- List<Long> idsToDelete = new ArrayList<>();
- for (GuidedAction guidedAction : getActions()) {
- if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID
- && guidedAction.isChecked()) {
- idsToDelete.add(guidedAction.getId());
- }
- }
- if (!idsToDelete.isEmpty()) {
- DvrManager dvrManager = TvSingletons.getSingletons(getActivity()).getDvrManager();
- dvrManager.removeRecordedPrograms(idsToDelete);
- }
- Toast.makeText(
- getContext(),
- getResources()
- .getQuantityString(
- R.plurals.dvr_msg_episodes_deleted,
- idsToDelete.size(),
- idsToDelete.size(),
- mRecordings.size()),
- Toast.LENGTH_LONG)
- .show();
- finishGuidedStepFragments();
+ delete();
} else if (actionId == GuidedAction.ACTION_ID_CANCEL) {
finishGuidedStepFragments();
} else if (actionId == ACTION_ID_SELECT_WATCHED) {
@@ -234,6 +217,51 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment {
};
}
+ private void delete() {
+ mIdsToDelete.clear();
+ for (GuidedAction guidedAction : getActions()) {
+ if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID
+ && guidedAction.isChecked()) {
+ mIdsToDelete.add(guidedAction.getId());
+ }
+ }
+ ((DvrSeriesDeletionActivity) getActivity()).setIdsToDelete(mIdsToDelete);
+ if (!PermissionUtils.hasWriteExternalStorage(getContext())
+ && doesAnySelectedRecordedProgramNeedWritePermission()) {
+ DvrUiHelper.showWriteStoragePermissionRationaleDialog(getActivity());
+ } else {
+ deleteSelectedIds();
+ }
+ }
+
+ private boolean doesAnySelectedRecordedProgramNeedWritePermission() {
+ for (RecordedProgram r : mRecordings) {
+ if (mIdsToDelete.contains(r.getId())
+ && DvrManager.isFile(r.getDataUri())
+ && !DvrManager.isFromBundledInput(r)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void deleteSelectedIds() {
+ if (!mIdsToDelete.isEmpty()) {
+ mDvrManager.removeRecordedPrograms(mIdsToDelete, true);
+ }
+ Toast.makeText(
+ getContext(),
+ getResources()
+ .getQuantityString(
+ R.plurals.dvr_msg_episodes_deleted,
+ mIdsToDelete.size(),
+ mIdsToDelete.size(),
+ mRecordings.size()),
+ Toast.LENGTH_LONG)
+ .show();
+ finishGuidedStepFragments();
+ }
+
private String getWatchedString(long watchedPositionMs, long durationMs) {
if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) {
return getResources()
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
index edb62c96..c6e26850 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
@@ -101,9 +101,9 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment {
String title = getString(R.string.dvr_series_recording_dialog_title);
Drawable icon;
if (!mHasConflict) {
- icon = getResources().getDrawable(R.drawable.ic_check_circle_white_48dp, null);
+ icon = getResources().getDrawable(R.drawable.quantum_ic_check_circle_white_48, null);
} else {
- icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null);
+ icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null);
}
return new GuidanceStylist.Guidance(title, getDescription(), null, icon);
}
diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
index e93387ab..1ab4c500 100644
--- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
@@ -126,7 +126,7 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment {
} else {
description = getString(R.string.dvr_stop_recording_dialog_description);
}
- Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null);
+ Drawable image = getResources().getDrawable(R.drawable.quantum_ic_warning_white_96, null);
return new Guidance(title, description, null, image);
}
diff --git a/src/com/android/tv/dvr/ui/DvrUiHelper.java b/src/com/android/tv/dvr/ui/DvrUiHelper.java
index 16afbdef..a121cf99 100644
--- a/src/com/android/tv/dvr/ui/DvrUiHelper.java
+++ b/src/com/android/tv/dvr/ui/DvrUiHelper.java
@@ -37,10 +37,10 @@ import android.text.TextUtils;
import android.text.style.TextAppearanceSpan;
import android.widget.ImageView;
import android.widget.Toast;
+
import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.TvSingletons;
-import com.android.tv.common.BuildConfig;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.recording.RecordingStorageStatusManager;
import com.android.tv.common.util.CommonUtils;
@@ -57,7 +57,6 @@ import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialog
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyScheduledDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelRecordDurationOptionDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelWatchConflictDialogFragment;
-import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrFutureProgramInfoDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrInsufficientSpaceErrorDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrNoFreeSpaceErrorDialogFragment;
@@ -65,15 +64,17 @@ import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrProgramConflictDialog
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrScheduleDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrSmallSizedStorageErrorDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrStopRecordingDialogFragment;
+import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrWriteStoragePermissionRationaleDialogFragment;
import com.android.tv.dvr.ui.browse.DvrBrowseActivity;
-import com.android.tv.dvr.ui.browse.DvrDetailsActivity;
import com.android.tv.dvr.ui.list.DvrHistoryActivity;
import com.android.tv.dvr.ui.list.DvrSchedulesActivity;
import com.android.tv.dvr.ui.list.DvrSchedulesFragment;
import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment;
import com.android.tv.dvr.ui.playback.DvrPlaybackActivity;
+import com.android.tv.ui.DetailsActivity;
import com.android.tv.util.ToastUtils;
import com.android.tv.util.Utils;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -241,13 +242,9 @@ public class DvrUiHelper {
}
/** Shows program information dialog. */
- public static void showProgramInfoDialog(Activity activity, Program program) {
- if (program == null || !BuildConfig.ENG) {
- return;
- }
- Bundle args = new Bundle();
- args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program);
- showDialogFragment(activity, new DvrFutureProgramInfoDialogFragment(), args, false, true);
+ public static void showWriteStoragePermissionRationaleDialog(Activity activity) {
+ showDialogFragment(activity, new DvrWriteStoragePermissionRationaleDialogFragment(),
+ new Bundle(), false, false);
}
/**
@@ -577,47 +574,43 @@ public class DvrUiHelper {
if (dvrItem == null) {
return;
}
- Intent intent = new Intent(activity, DvrDetailsActivity.class);
+ Intent intent = new Intent(activity, DetailsActivity.class);
long recordingId;
int viewType;
if (dvrItem instanceof ScheduledRecording) {
ScheduledRecording schedule = (ScheduledRecording) dvrItem;
recordingId = schedule.getId();
if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
- viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW;
+ viewType = DetailsActivity.SCHEDULED_RECORDING_VIEW;
} else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
- viewType = DvrDetailsActivity.CURRENT_RECORDING_VIEW;
+ viewType = DetailsActivity.CURRENT_RECORDING_VIEW;
} else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED
&& schedule.getRecordedProgramId() != null) {
recordingId = schedule.getRecordedProgramId();
- viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW;
+ viewType = DetailsActivity.RECORDED_PROGRAM_VIEW;
} else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
- viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW;
+ viewType = DetailsActivity.SCHEDULED_RECORDING_VIEW;
hideViewSchedule = true;
- // TODO(b/72638385): pass detailed error message
- intent.putExtra(
- DvrDetailsActivity.EXTRA_FAILED_MESSAGE,
- activity.getString(R.string.dvr_recording_failed));
} else {
return;
}
} else if (dvrItem instanceof RecordedProgram) {
recordingId = ((RecordedProgram) dvrItem).getId();
- viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW;
+ viewType = DetailsActivity.RECORDED_PROGRAM_VIEW;
} else if (dvrItem instanceof SeriesRecording) {
recordingId = ((SeriesRecording) dvrItem).getId();
- viewType = DvrDetailsActivity.SERIES_RECORDING_VIEW;
+ viewType = DetailsActivity.SERIES_RECORDING_VIEW;
} else {
return;
}
- intent.putExtra(DvrDetailsActivity.RECORDING_ID, recordingId);
- intent.putExtra(DvrDetailsActivity.DETAILS_VIEW_TYPE, viewType);
- intent.putExtra(DvrDetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule);
+ intent.putExtra(DetailsActivity.RECORDING_ID, recordingId);
+ intent.putExtra(DetailsActivity.DETAILS_VIEW_TYPE, viewType);
+ intent.putExtra(DetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule);
Bundle bundle = null;
if (imageView != null) {
bundle =
ActivityOptionsCompat.makeSceneTransitionAnimation(
- activity, imageView, DvrDetailsActivity.SHARED_ELEMENT_NAME)
+ activity, imageView, DetailsActivity.SHARED_ELEMENT_NAME)
.toBundle();
}
activity.startActivity(intent, bundle);
diff --git a/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java b/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java
new file mode 100644
index 00000000..c93f5831
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2018 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.content.res.Resources;
+import android.os.Bundle;
+import android.support.v17.leanback.widget.GuidanceStylist;
+import android.support.v17.leanback.widget.GuidedAction;
+
+import com.android.tv.R;
+
+import java.util.List;
+
+/**
+ * A fragment which shows the rationale when requesting android.permission.WRITE_EXTERNAL_STORAGE.
+ */
+public class DvrWriteStoragePermissionRationaleFragment extends DvrGuidedStepFragment {
+ @Override
+ public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
+ Resources res = getContext().getResources();
+ String title = res.getString(R.string.write_storage_permission_rationale_title);
+ String description = res.getString(R.string.write_storage_permission_rationale_description);
+ return new GuidanceStylist.Guidance(title, description, null, null);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ Activity activity = getActivity();
+ actions.add(
+ new GuidedAction.Builder(activity)
+ .id(GuidedAction.ACTION_ID_OK)
+ .title(android.R.string.ok)
+ .build());
+ }
+
+ @Override
+ public void onTrackedGuidedActionClicked(GuidedAction action) {
+ dismissDialog();
+ }
+
+ @Override
+ public String getTrackerPrefix() {
+ return "DvrWriteStoragePermissionRationaleFragment";
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
index f3a6fea4..41ace9a4 100644
--- a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
+++ b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
@@ -27,9 +27,11 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
-// This class is adapted from Leanback's library, which does not support action icon with one-line
-// label. This class modified its getPresenter method to support the above situation.
-class ActionPresenterSelector extends PresenterSelector {
+/**
+ * This class is adapted from Leanback's library, which does not support action icon with one-line
+ * label. This class modified its getPresenter method to support the above situation.
+ */
+public class ActionPresenterSelector extends PresenterSelector {
private final Presenter mOneLineActionPresenter = new OneLineActionPresenter();
private final Presenter mTwoLineActionPresenter = new TwoLineActionPresenter();
private final Presenter[] mPresenters =
diff --git a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
index 7e7e1f75..8c311d68 100644
--- a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
@@ -18,23 +18,34 @@ package com.android.tv.dvr.ui.browse;
import android.content.Context;
import android.content.res.Resources;
+import android.media.tv.TvInputManager;
import android.support.v17.leanback.widget.Action;
import android.support.v17.leanback.widget.OnActionClickedListener;
import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
import com.android.tv.R;
import com.android.tv.TvSingletons;
+import com.android.tv.common.flags.has.HasConcurrentDvrPlaybackFlags;
import com.android.tv.dialog.HalfSizedDialogFragment;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrWatchedPositionManager;
+import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.ui.DvrStopRecordingFragment;
import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
/** {@link RecordingDetailsFragment} for current recording in DVR. */
public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
private static final int ACTION_STOP_RECORDING = 1;
+ private static final int ACTION_RESUME_PLAYING = 2;
+ private static final int ACTION_PLAY_FROM_BEGINNING = 3;
private DvrDataManager mDvrDataManger;
+ private RecordedProgram mRecordedProgram;
+ private DvrWatchedPositionManager mDvrWatchedPositionManager;
+ private ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+ private boolean mPaused;
private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener =
new DvrDataManager.ScheduledRecordingListener() {
@Override
@@ -68,10 +79,32 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
super.onAttach(context);
mDvrDataManger = TvSingletons.getSingletons(context).getDvrDataManager();
mDvrDataManger.addScheduledRecordingListener(mScheduledRecordingListener);
+ mDvrWatchedPositionManager =
+ TvSingletons.getSingletons(getActivity()).getDvrWatchedPositionManager();
+ mConcurrentDvrPlaybackFlags = HasConcurrentDvrPlaybackFlags.fromContext(context);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mPaused) {
+ updateActions();
+ mPaused = false;
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mPaused = true;
}
@Override
protected SparseArrayObjectAdapter onCreateActionsAdapter() {
+ Long recordedProgramId = getRecording().getRecordedProgramId();
+ if (recordedProgramId != null) {
+ mRecordedProgram = mDvrDataManger.getRecordedProgram(recordedProgramId);
+ }
SparseArrayObjectAdapter adapter =
new SparseArrayObjectAdapter(new ActionPresenterSelector());
Resources res = getResources();
@@ -82,6 +115,35 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
res.getString(R.string.dvr_detail_stop_recording),
null,
res.getDrawable(R.drawable.lb_ic_stop)));
+ if (mConcurrentDvrPlaybackFlags.enabled()
+ && mRecordedProgram != null
+ && mRecordedProgram.isPartial()) {
+ if (mDvrWatchedPositionManager.getWatchedStatus(mRecordedProgram)
+ == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) {
+ adapter.set(
+ ACTION_RESUME_PLAYING,
+ new Action(
+ ACTION_RESUME_PLAYING,
+ res.getString(R.string.dvr_detail_resume_play),
+ null,
+ res.getDrawable(R.drawable.lb_ic_play)));
+ adapter.set(
+ ACTION_PLAY_FROM_BEGINNING,
+ new Action(
+ ACTION_PLAY_FROM_BEGINNING,
+ res.getString(R.string.dvr_detail_play_from_beginning),
+ null,
+ res.getDrawable(R.drawable.lb_ic_replay)));
+ } else {
+ adapter.set(
+ ACTION_PLAY_FROM_BEGINNING,
+ new Action(
+ ACTION_PLAY_FROM_BEGINNING,
+ res.getString(R.string.dvr_detail_watch),
+ null,
+ res.getDrawable(R.drawable.lb_ic_play)));
+ }
+ }
return adapter;
}
@@ -107,6 +169,13 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
}
}
});
+ } else if (action.getId() == ACTION_RESUME_PLAYING) {
+ startPlayback(
+ mRecordedProgram,
+ mDvrWatchedPositionManager.getWatchedPosition(
+ mRecordedProgram.getId()));
+ } else if (action.getId() == ACTION_PLAY_FROM_BEGINNING) {
+ startPlayback(mRecordedProgram, TvInputManager.TIME_SHIFT_INVALID_TIME);
}
}
};
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContent.java b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
index cba6293b..e179743c 100644
--- a/src/com/android/tv/dvr/ui/browse/DetailsContent.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
@@ -22,6 +22,7 @@ import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.android.tv.R;
import com.android.tv.TvSingletons;
+import com.android.tv.data.Program;
import com.android.tv.data.api.Channel;
import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.dvr.data.ScheduledRecording;
@@ -29,7 +30,7 @@ import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.ui.DvrUiHelper;
/** A class for details content. */
-class DetailsContent {
+public class DetailsContent {
/** Constant for invalid time. */
public static final long INVALID_TIME = -1;
@@ -40,6 +41,7 @@ class DetailsContent {
private String mLogoImageUri;
private String mBackgroundImageUri;
private boolean mUsingChannelLogo;
+ private boolean mShowErrorMessage;
static DetailsContent createFromRecordedProgram(
Context context, RecordedProgram recordedProgram) {
@@ -59,6 +61,23 @@ class DetailsContent {
.build(context);
}
+ public static DetailsContent createFromProgram(Context context, Program program) {
+ return new DetailsContent.Builder()
+ .setChannelId(program.getChannelId())
+ .setProgramTitle(program.getTitle())
+ .setSeasonNumber(program.getSeasonNumber())
+ .setEpisodeNumber(program.getEpisodeNumber())
+ .setStartTimeUtcMillis(program.getStartTimeUtcMillis())
+ .setEndTimeUtcMillis(program.getEndTimeUtcMillis())
+ .setDescription(
+ TextUtils.isEmpty(program.getLongDescription())
+ ? program.getDescription()
+ : program.getLongDescription())
+ .setPosterArtUri(program.getPosterArtUri())
+ .setThumbnailUri(program.getThumbnailUri())
+ .build(context);
+ }
+
static DetailsContent createFromSeriesRecording(
Context context, SeriesRecording seriesRecording) {
return new DetailsContent.Builder()
@@ -79,37 +98,9 @@ class DetailsContent {
TvSingletons.getSingletons(context)
.getChannelDataManager()
.getChannel(scheduledRecording.getChannelId());
- String description =
- !TextUtils.isEmpty(scheduledRecording.getProgramDescription())
- ? scheduledRecording.getProgramDescription()
- : scheduledRecording.getProgramLongDescription();
- if (TextUtils.isEmpty(description)) {
- description = channel != null ? channel.getDescription() : null;
- }
- return new DetailsContent.Builder()
- .setChannelId(scheduledRecording.getChannelId())
- .setProgramTitle(scheduledRecording.getProgramTitle())
- .setSeasonNumber(scheduledRecording.getSeasonNumber())
- .setEpisodeNumber(scheduledRecording.getEpisodeNumber())
- .setStartTimeUtcMillis(scheduledRecording.getStartTimeMs())
- .setEndTimeUtcMillis(scheduledRecording.getEndTimeMs())
- .setDescription(description)
- .setPosterArtUri(scheduledRecording.getProgramPosterArtUri())
- .setThumbnailUri(scheduledRecording.getProgramThumbnailUri())
- .build(context);
- }
-
- static DetailsContent createFromFailedScheduledRecording(
- Context context, ScheduledRecording scheduledRecording, String errMsg) {
- Channel channel =
- TvSingletons.getSingletons(context)
- .getChannelDataManager()
- .getChannel(scheduledRecording.getChannelId());
String description;
- if (scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED
- && errMsg != null) {
- description = errMsg
- + " (Error code: " + scheduledRecording.getFailedReason() + ")";
+ if (scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+ description = getErrorMessage(context, scheduledRecording);
} else {
description =
!TextUtils.isEmpty(scheduledRecording.getProgramDescription())
@@ -129,9 +120,39 @@ class DetailsContent {
.setDescription(description)
.setPosterArtUri(scheduledRecording.getProgramPosterArtUri())
.setThumbnailUri(scheduledRecording.getProgramThumbnailUri())
+ .setShowErrorMessage(
+ scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED)
.build(context);
}
+ private static String getErrorMessage(Context context, ScheduledRecording recording) {
+ int reason = recording.getFailedReason() == null
+ ? ScheduledRecording.FAILED_REASON_OTHER
+ : recording.getFailedReason();
+ switch (reason) {
+ case ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED:
+ return context.getString(R.string.dvr_recording_failed_not_started);
+ case ScheduledRecording.FAILED_REASON_RESOURCE_BUSY:
+ return context.getString(R.string.dvr_recording_failed_resource_busy);
+ case ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE:
+ return context.getString(
+ R.string.dvr_recording_failed_input_unavailable,
+ recording.getInputId());
+ case ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED:
+ return context.getString(R.string.dvr_recording_failed_input_dvr_unsupported);
+ case ScheduledRecording.FAILED_REASON_INSUFFICIENT_SPACE:
+ return context.getString(R.string.dvr_recording_failed_insufficient_space);
+ case ScheduledRecording.FAILED_REASON_OTHER: // fall through
+ case ScheduledRecording.FAILED_REASON_NOT_FINISHED: // fall through
+ case ScheduledRecording.FAILED_REASON_SCHEDULER_STOPPED: // fall through
+ case ScheduledRecording.FAILED_REASON_INVALID_CHANNEL: // fall through
+ case ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT: // fall through
+ case ScheduledRecording.FAILED_REASON_CONNECTION_FAILED: // fall through
+ default:
+ return context.getString(R.string.dvr_recording_failed_system_failure, reason);
+ }
+ }
+
private DetailsContent() {}
/** Returns title. */
@@ -169,6 +190,11 @@ class DetailsContent {
return mUsingChannelLogo;
}
+ /** Returns if the error message should be shown. */
+ public boolean shouldShowErrorMessage() {
+ return mShowErrorMessage;
+ }
+
/** Copies other details content. */
public void copyFrom(DetailsContent other) {
if (this == other) {
@@ -181,6 +207,7 @@ class DetailsContent {
mLogoImageUri = other.mLogoImageUri;
mBackgroundImageUri = other.mBackgroundImageUri;
mUsingChannelLogo = other.mUsingChannelLogo;
+ mShowErrorMessage = other.mShowErrorMessage;
}
/** A class for building details content. */
@@ -266,6 +293,11 @@ class DetailsContent {
return this;
}
+ private Builder setShowErrorMessage(boolean showErrorMessage) {
+ mDetailsContent.mShowErrorMessage = showErrorMessage;
+ return this;
+ }
+
private void createStyledTitle(Context context, Channel channel) {
CharSequence title =
DvrUiHelper.getStyledTitleWithEpisodeNumber(
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
index aec8c411..6b5fd1fd 100644
--- a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
@@ -45,12 +45,13 @@ import com.android.tv.util.Utils;
* The latter class are re-used to provide a customized version of {@link
* android.support.v17.leanback.widget.DetailsOverviewRow}.
*/
-class DetailsContentPresenter extends Presenter {
+public class DetailsContentPresenter extends Presenter {
/** The ViewHolder for the {@link DetailsContentPresenter}. */
public static class ViewHolder extends Presenter.ViewHolder {
final TextView mTitle;
final TextView mSubtitle;
final LinearLayout mDescriptionContainer;
+ final LinearLayout mErrorMessage;
final TextView mBody;
final TextView mReadMoreView;
final int mTitleMargin;
@@ -150,6 +151,8 @@ class DetailsContentPresenter extends Presenter {
});
mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title);
mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle);
+ mErrorMessage =
+ (LinearLayout) view.findViewById(R.id.dvr_details_description_error_message);
mBody = (TextView) view.findViewById(R.id.dvr_details_description_body);
mDescriptionContainer =
(LinearLayout) view.findViewById(R.id.dvr_details_description_container);
@@ -321,6 +324,9 @@ class DetailsContentPresenter extends Presenter {
if (TextUtils.isEmpty(detailsContent.getDescription())) {
vh.mBody.setVisibility(View.GONE);
} else {
+ if (detailsContent.shouldShowErrorMessage()) {
+ vh.mErrorMessage.setVisibility(View.VISIBLE);
+ }
vh.mBody.setText(detailsContent.getDescription());
vh.mBody.setVisibility(View.VISIBLE);
vh.mBody.setLineSpacing(
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
index 849360b8..4e41daee 100644
--- a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
@@ -24,7 +24,7 @@ import android.os.Handler;
import android.support.v17.leanback.app.BackgroundManager;
/** The Background Helper. */
-class DetailsViewBackgroundHelper {
+public class DetailsViewBackgroundHelper {
// Background delay serves to avoid kicking off expensive bitmap loading
// in case multiple backgrounds are set in quick succession.
private static final int SET_BACKGROUND_DELAY_MS = 100;
diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
index 6cc1c7a1..5743ea5c 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
@@ -22,9 +22,15 @@ import android.media.tv.TvInputManager;
import android.os.Bundle;
import com.android.tv.R;
import com.android.tv.Starter;
+import com.android.tv.perf.PerformanceMonitorManagerFactory;
/** {@link android.app.Activity} for DVR UI. */
public class DvrBrowseActivity extends Activity {
+
+ {
+ PerformanceMonitorManagerFactory.create().getStartupMeasure().onActivityInit();
+ }
+
private DvrBrowseFragment mFragment;
@Override
diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
index 40b3a1f0..17ba1939 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
@@ -31,9 +31,7 @@ import android.support.v17.leanback.widget.TitleViewAdapter;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
-
import com.android.tv.R;
-import com.android.tv.TvFeatures;
import com.android.tv.TvSingletons;
import com.android.tv.data.GenreItems;
import com.android.tv.dvr.DvrDataManager;
@@ -47,7 +45,7 @@ import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.ui.SortedArrayAdapter;
-
+import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@@ -66,7 +64,7 @@ public class DvrBrowseFragment extends BrowseFragment
private static final String TAG = "DvrBrowseFragment";
private static final boolean DEBUG = false;
- private static final int MAX_RECENT_ITEM_COUNT = 10;
+ private static final int MAX_RECENT_ITEM_COUNT = 4;
private static final int MAX_SCHEDULED_ITEM_COUNT = 4;
private boolean mShouldShowScheduleRow;
@@ -104,93 +102,84 @@ public class DvrBrowseFragment extends BrowseFragment
};
private final Comparator<Object> RECORDED_PROGRAM_COMPARATOR =
- new Comparator<Object>() {
- @Override
- public int compare(Object lhs, Object rhs) {
- if (lhs instanceof SeriesRecording) {
- lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId());
- }
- if (rhs instanceof SeriesRecording) {
- rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId());
- }
- if (lhs instanceof RecordedProgram) {
- if (rhs instanceof RecordedProgram) {
- return RecordedProgram.START_TIME_THEN_ID_COMPARATOR
- .reversed()
- .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
- } else {
- return -1;
- }
- } else if (rhs instanceof RecordedProgram) {
- return 1;
+ (Object lhs, Object rhs) -> {
+ if (lhs instanceof SeriesRecording) {
+ lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId());
+ }
+ if (rhs instanceof SeriesRecording) {
+ rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId());
+ }
+ if (lhs instanceof RecordedProgram) {
+ if (rhs instanceof RecordedProgram) {
+ return RecordedProgram.START_TIME_THEN_ID_COMPARATOR
+ .reversed()
+ .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
} else {
- return 0;
+ return -1;
}
+ } else if (rhs instanceof RecordedProgram) {
+ return 1;
+ } else {
+ return 0;
}
};
private static final Comparator<Object> SCHEDULE_COMPARATOR =
- new Comparator<Object>() {
- @Override
- public int compare(Object lhs, Object rhs) {
- if (lhs instanceof ScheduledRecording) {
- if (rhs instanceof ScheduledRecording) {
- return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
- .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
- } else {
- return -1;
- }
- } else if (rhs instanceof ScheduledRecording) {
- return 1;
+ (Object lhs, Object rhs) -> {
+ if (lhs instanceof ScheduledRecording) {
+ if (rhs instanceof ScheduledRecording) {
+ return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
+ .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
} else {
- return 0;
+ return -1;
}
+ } else if (rhs instanceof ScheduledRecording) {
+ return 1;
+ } else {
+ return 0;
}
};
static final Comparator<Object> RECENT_ROW_COMPARATOR =
- new Comparator<Object>() {
- @Override
- public int compare(Object lhs, Object rhs) {
- if (lhs instanceof ScheduledRecording) {
- if (rhs instanceof ScheduledRecording) {
- return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
- .reversed()
- .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
- } else if (rhs instanceof RecordedProgram) {
- ScheduledRecording scheduled = (ScheduledRecording) lhs;
- RecordedProgram recorded = (RecordedProgram) rhs;
- int compare =
- Long.compare(
- recorded.getStartTimeUtcMillis(),
- scheduled.getStartTimeMs());
- // recorded program first when the start times are the same
- return compare == 0 ? 1 : compare;
- } else {
- return -1;
- }
- } else if (lhs instanceof RecordedProgram) {
- if (rhs instanceof RecordedProgram) {
- return RecordedProgram.START_TIME_THEN_ID_COMPARATOR
- .reversed()
- .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
- } else if (rhs instanceof ScheduledRecording) {
- RecordedProgram recorded = (RecordedProgram) lhs;
- ScheduledRecording scheduled = (ScheduledRecording) rhs;
- int compare =
- Long.compare(
- scheduled.getStartTimeMs(),
- recorded.getStartTimeUtcMillis());
- // recorded program first when the start times are the same
- return compare == 0 ? -1 : compare;
- } else {
- return -1;
- }
+ (Object lhs, Object rhs) -> {
+ if (lhs instanceof ScheduledRecording) {
+ if (rhs instanceof ScheduledRecording) {
+ return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
+ .reversed()
+ .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
+ } else if (rhs instanceof RecordedProgram) {
+ ScheduledRecording scheduled = (ScheduledRecording) lhs;
+ RecordedProgram recorded = (RecordedProgram) rhs;
+ int compare =
+ Long.compare(
+ recorded.getStartTimeUtcMillis(),
+ scheduled.getStartTimeMs());
+ // recorded program first when the start times are the same
+ return compare == 0 ? 1 : compare;
+ } else {
+ return -1;
+ }
+ } else if (lhs instanceof RecordedProgram) {
+ if (rhs instanceof RecordedProgram) {
+ return RecordedProgram.START_TIME_THEN_ID_COMPARATOR
+ .reversed()
+ .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
+ } else if (rhs instanceof ScheduledRecording) {
+ RecordedProgram recorded = (RecordedProgram) lhs;
+ ScheduledRecording scheduled = (ScheduledRecording) rhs;
+ int compare =
+ Long.compare(
+ scheduled.getStartTimeMs(),
+ recorded.getStartTimeUtcMillis());
+ // recorded program first when the start times are the same
+ return compare == 0 ? -1 : compare;
} else {
- return !(rhs instanceof RecordedProgram)
- && !(rhs instanceof ScheduledRecording)
- ? 0 : 1;
+ return -1;
}
+ } else {
+ return !(rhs instanceof RecordedProgram) && !(rhs instanceof ScheduledRecording)
+ ? 0
+ : 1;
}
};
@@ -207,13 +196,7 @@ public class DvrBrowseFragment extends BrowseFragment
}
};
- private final Runnable mUpdateRowsRunnable =
- new Runnable() {
- @Override
- public void run() {
- updateRows();
- }
- };
+ private final Runnable mUpdateRowsRunnable = this::updateRows;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -233,13 +216,10 @@ public class DvrBrowseFragment extends BrowseFragment
SeriesRecording.class, new SeriesRecordingPresenter(context))
.addClassPresenter(
FullScheduleCardHolder.class,
- new FullSchedulesCardPresenter(context));
+ new FullSchedulesCardPresenter(context))
+ .addClassPresenter(
+ DvrHistoryCardHolder.class, new DvrHistoryCardPresenter(context));
- if (TvFeatures.DVR_FAILED_LIST.isEnabled(context)) {
- mPresenterSelector.addClassPresenter(
- DvrHistoryCardHolder.class,
- new DvrHistoryCardPresenter(context));
- }
mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context)));
mGenreLabels.add(getString(R.string.dvr_main_others));
prepareUiElements();
@@ -310,7 +290,9 @@ public class DvrBrowseFragment extends BrowseFragment
@Override
public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {
for (RecordedProgram recordedProgram : recordedPrograms) {
- handleRecordedProgramChanged(recordedProgram);
+ if (recordedProgram.isVisible()) {
+ handleRecordedProgramChanged(recordedProgram);
+ }
}
postUpdateRows();
}
@@ -340,6 +322,9 @@ public class DvrBrowseFragment extends BrowseFragment
public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
for (ScheduledRecording scheduleRecording : scheduledRecordings) {
mScheduleAdapter.remove(scheduleRecording);
+ if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+ mRecentAdapter.remove(scheduleRecording);
+ }
}
}
@@ -351,6 +336,9 @@ public class DvrBrowseFragment extends BrowseFragment
} else {
mScheduleAdapter.removeWithId(scheduleRecording);
}
+ if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+ mRecentAdapter.change(scheduleRecording);
+ }
}
}
@@ -443,16 +431,17 @@ public class DvrBrowseFragment extends BrowseFragment
mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER);
// Recorded Programs.
for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
- handleRecordedProgramAdded(recordedProgram, false);
- }
- if (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext())) {
- // only get failed recordings
- for (ScheduledRecording scheduledRecording
- : mDvrDataManager.getFailedScheduledRecordings()) {
- onScheduledRecordingAdded(scheduledRecording);
+ if (recordedProgram.isVisible()) {
+ handleRecordedProgramAdded(recordedProgram, false);
}
- mRecentAdapter.addExtraItem(DvrHistoryCardHolder.DVR_HISTORY_CARD_HOLDER);
}
+ // only get failed recordings
+ for (ScheduledRecording scheduledRecording :
+ mDvrDataManager.getFailedScheduledRecordings()) {
+ onScheduledRecordingAdded(scheduledRecording);
+ }
+ mRecentAdapter.addExtraItem(DvrHistoryCardHolder.DVR_HISTORY_CARD_HOLDER);
+
// Series Recordings. Series recordings should be added after recorded programs, because
// we build series recordings' latest program information while adding recorded
// programs.
@@ -592,9 +581,9 @@ public class DvrBrowseFragment extends BrowseFragment
}
}
- private List<RecordedProgramAdapter> getGenreAdapters(String[] genres) {
+ private List<RecordedProgramAdapter> getGenreAdapters(ImmutableList<String> genres) {
List<RecordedProgramAdapter> result = new ArrayList<>();
- if (genres == null || genres.length == 0) {
+ if (genres == null || genres.isEmpty()) {
result.add(mGenreAdapters[mGenreAdapters.length - 1]);
} else {
for (String genre : genres) {
@@ -642,8 +631,8 @@ public class DvrBrowseFragment extends BrowseFragment
private void updateRows() {
int visibleRowsCount = 1; // Schedule's Row will never be empty
- int recentRowMinSize = TvFeatures.DVR_FAILED_LIST.isEnabled(getContext()) ? 1 : 0;
- if (mRecentAdapter.size() <= recentRowMinSize) {
+ if (mRecentAdapter.size() <= 1) {
+ // remove the row if there is only the DVR history card
mRowsAdapter.remove(mRecentRow);
} else {
if (mRowsAdapter.indexOf(mRecentRow) < 0) {
@@ -673,6 +662,9 @@ public class DvrBrowseFragment extends BrowseFragment
}
}
}
+ if (getSelectedPosition() >= mRowsAdapter.size()) {
+ setSelectedPosition(mRecentAdapter.size() - 1);
+ }
}
private boolean needToShowScheduledRecording(ScheduledRecording recording) {
@@ -713,16 +705,13 @@ public class DvrBrowseFragment extends BrowseFragment
SeriesAdapter() {
super(
mPresenterSelector,
- new Comparator<SeriesRecording>() {
- @Override
- public int compare(SeriesRecording lhs, SeriesRecording rhs) {
- if (lhs.isStopped() && !rhs.isStopped()) {
- return 1;
- } else if (!lhs.isStopped() && rhs.isStopped()) {
- return -1;
- }
- return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs);
+ (SeriesRecording lhs, SeriesRecording rhs) -> {
+ if (lhs.isStopped() && !rhs.isStopped()) {
+ return 1;
+ } else if (!lhs.isStopped() && rhs.isStopped()) {
+ return -1;
}
+ return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs);
});
}
diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
deleted file mode 100644
index 0336b319..00000000
--- a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * 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.browse;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.v17.leanback.app.DetailsFragment;
-import android.transition.Transition;
-import android.transition.Transition.TransitionListener;
-import android.view.View;
-import com.android.tv.R;
-import com.android.tv.Starter;
-import com.android.tv.dialog.PinDialogFragment;
-
-/** Activity to show details view in DVR. */
-public class DvrDetailsActivity extends Activity implements PinDialogFragment.OnPinCheckedListener {
- /** Name of record id added to the Intent. */
- public static final String RECORDING_ID = "record_id";
-
- /**
- * Name of flag added to the Intent to determine if details view should hide "View schedule"
- * button.
- */
- public static final String HIDE_VIEW_SCHEDULE = "hide_view_schedule";
-
- /** Name of details view's type added to the intent. */
- public static final String DETAILS_VIEW_TYPE = "details_view_type";
-
- /** Name of shared element between activities. */
- public static final String SHARED_ELEMENT_NAME = "shared_element";
-
- /** Name of error message of a failed recording */
- public static final String EXTRA_FAILED_MESSAGE = "failed_message";
-
- /** CURRENT_RECORDING_VIEW refers to Current Recordings in DVR. */
- public static final int CURRENT_RECORDING_VIEW = 1;
-
- /** SCHEDULED_RECORDING_VIEW refers to Scheduled Recordings in DVR. */
- public static final int SCHEDULED_RECORDING_VIEW = 2;
-
- /** RECORDED_PROGRAM_VIEW refers to Recorded programs in DVR. */
- public static final int RECORDED_PROGRAM_VIEW = 3;
-
- /** SERIES_RECORDING_VIEW refers to series recording in DVR. */
- public static final int SERIES_RECORDING_VIEW = 4;
-
- private PinDialogFragment.OnPinCheckedListener mOnPinCheckedListener;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- Starter.start(this);
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_dvr_details);
- long recordId = getIntent().getLongExtra(RECORDING_ID, -1);
- int detailsViewType = getIntent().getIntExtra(DETAILS_VIEW_TYPE, -1);
- boolean hideViewSchedule = getIntent().getBooleanExtra(HIDE_VIEW_SCHEDULE, false);
- String failedMsg = getIntent().getStringExtra(EXTRA_FAILED_MESSAGE);
- if (recordId != -1 && detailsViewType != -1 && savedInstanceState == null) {
- Bundle args = new Bundle();
- args.putLong(RECORDING_ID, recordId);
- DetailsFragment detailsFragment = null;
- if (detailsViewType == CURRENT_RECORDING_VIEW) {
- detailsFragment = new CurrentRecordingDetailsFragment();
- } else if (detailsViewType == SCHEDULED_RECORDING_VIEW) {
- args.putBoolean(HIDE_VIEW_SCHEDULE, hideViewSchedule);
- args.putString(EXTRA_FAILED_MESSAGE, failedMsg);
- detailsFragment = new ScheduledRecordingDetailsFragment();
- } else if (detailsViewType == RECORDED_PROGRAM_VIEW) {
- detailsFragment = new RecordedProgramDetailsFragment();
- } else if (detailsViewType == SERIES_RECORDING_VIEW) {
- detailsFragment = new SeriesRecordingDetailsFragment();
- }
- detailsFragment.setArguments(args);
- getFragmentManager()
- .beginTransaction()
- .replace(R.id.dvr_details_view_frame, detailsFragment)
- .commit();
- }
-
- // This is a workaround for the focus on O device
- addTransitionListener();
- }
-
- @Override
- public void onPinChecked(boolean checked, int type, String rating) {
- if (mOnPinCheckedListener != null) {
- mOnPinCheckedListener.onPinChecked(checked, type, rating);
- }
- }
-
- void setOnPinCheckListener(PinDialogFragment.OnPinCheckedListener listener) {
- mOnPinCheckedListener = listener;
- }
-
- private void addTransitionListener() {
- getWindow()
- .getSharedElementEnterTransition()
- .addListener(
- new TransitionListener() {
- @Override
- public void onTransitionStart(Transition transition) {
- // Do nothing
- }
-
- @Override
- public void onTransitionEnd(Transition transition) {
- View actions = findViewById(R.id.details_overview_actions);
- if (actions != null) {
- actions.requestFocus();
- }
- }
-
- @Override
- public void onTransitionCancel(Transition transition) {
- // Do nothing
-
- }
-
- @Override
- public void onTransitionPause(Transition transition) {
- // Do nothing
- }
-
- @Override
- public void onTransitionResume(Transition transition) {
- // Do nothing
- }
- });
- }
-}
diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
index 8f4e4dab..f90981f0 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
@@ -47,8 +47,10 @@ import com.android.tv.dialog.PinDialogFragment.OnPinCheckedListener;
import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.parental.ParentalControlSettings;
+import com.android.tv.ui.DetailsActivity;
import com.android.tv.util.ToastUtils;
import com.android.tv.util.images.ImageLoader;
+import com.google.common.collect.ImmutableList;
import java.io.File;
abstract class DvrDetailsFragment extends DetailsFragment {
@@ -89,7 +91,7 @@ abstract class DvrDetailsFragment extends DetailsFragment {
rowPresenter.setBackgroundColor(
getResources().getColor(R.color.common_tv_background, null));
rowPresenter.setSharedElementEnterTransition(
- getActivity(), DvrDetailsActivity.SHARED_ELEMENT_NAME);
+ getActivity(), DetailsActivity.SHARED_ELEMENT_NAME);
rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener());
mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter));
setAdapter(mRowsAdapter);
@@ -221,7 +223,7 @@ abstract class DvrDetailsFragment extends DetailsFragment {
checkPinToPlay(recordedProgram, seekTimeMs);
return;
}
- TvContentRating[] ratings = recordedProgram.getContentRatings();
+ ImmutableList<TvContentRating> ratings = recordedProgram.getContentRatings();
TvContentRating blockRatings = parental.getBlockedRating(ratings);
if (blockRatings != null) {
checkPinToPlay(recordedProgram, seekTimeMs);
@@ -245,15 +247,14 @@ abstract class DvrDetailsFragment extends DetailsFragment {
}
private void checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs) {
- SoftPreconditions.checkState(getActivity() instanceof DvrDetailsActivity);
- if (getActivity() instanceof DvrDetailsActivity) {
- ((DvrDetailsActivity) getActivity())
+ SoftPreconditions.checkState(getActivity() instanceof DetailsActivity);
+ if (getActivity() instanceof DetailsActivity) {
+ ((DetailsActivity) getActivity())
.setOnPinCheckListener(
new OnPinCheckedListener() {
@Override
public void onPinChecked(boolean checked, int type, String rating) {
- ((DvrDetailsActivity) getActivity())
- .setOnPinCheckListener(null);
+ ((DetailsActivity) getActivity()).setOnPinCheckListener(null);
if (checked
&& type
== PinDialogFragment
diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
index 47b1a198..bf963547 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
@@ -24,10 +24,13 @@ import android.support.v17.leanback.widget.OnActionClickedListener;
import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
import com.android.tv.R;
import com.android.tv.TvSingletons;
+import com.android.tv.common.util.PermissionUtils;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.ui.DetailsActivity;
/** {@link android.support.v17.leanback.app.DetailsFragment} for recorded program in DVR. */
public class RecordedProgramDetailsFragment extends DvrDetailsFragment
@@ -80,7 +83,7 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment
@Override
protected boolean onLoadRecordingDetails(Bundle args) {
- long recordedProgramId = args.getLong(DvrDetailsActivity.RECORDING_ID);
+ long recordedProgramId = args.getLong(DetailsActivity.RECORDING_ID);
mRecordedProgram = mDvrDataManager.getRecordedProgram(recordedProgramId);
return mRecordedProgram != null;
}
@@ -138,15 +141,24 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment
mDvrWatchedPositionManager.getWatchedPosition(
mRecordedProgram.getId()));
} else if (action.getId() == ACTION_DELETE_RECORDING) {
- DvrManager dvrManager =
- TvSingletons.getSingletons(getActivity()).getDvrManager();
- dvrManager.removeRecordedProgram(mRecordedProgram);
- getActivity().finish();
+ delete();
}
}
};
}
+ private void delete() {
+ if (!PermissionUtils.hasWriteExternalStorage(getContext())
+ && DvrManager.isFile(mRecordedProgram.getDataUri())
+ && !DvrManager.isFromBundledInput(mRecordedProgram)) {
+ DvrUiHelper.showWriteStoragePermissionRationaleDialog(getActivity());
+ } else {
+ DvrManager dvrManager = TvSingletons.getSingletons(getActivity()).getDvrManager();
+ dvrManager.removeRecordedProgram(mRecordedProgram, true);
+ getActivity().finish();
+ }
+ }
+
@Override
public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {}
diff --git a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
index fe3c52d9..c83ceaf0 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
@@ -48,11 +48,10 @@ public class RecordingCardView extends BaseCardView {
private final int mImageWidth;
private final int mImageHeight;
private String mImageUri;
+ private final ImageView mContentIconView;
private final TextView mMajorContentView;
private final TextView mMinorContentView;
private final ProgressBar mProgressBar;
- private final View mAffiliatedIconContainer;
- private final ImageView mAffiliatedIcon;
private final Drawable mDefaultImage;
private final FrameLayout mTitleArea;
private final TextView mFoldedTitleView;
@@ -94,8 +93,7 @@ public class RecordingCardView extends BaseCardView {
mImageWidth = imageWidth;
mImageHeight = imageHeight;
mProgressBar = (ProgressBar) findViewById(R.id.recording_progress);
- mAffiliatedIconContainer = findViewById(R.id.affiliated_icon_container);
- mAffiliatedIcon = (ImageView) findViewById(R.id.affiliated_icon);
+ mContentIconView = (ImageView) findViewById(R.id.content_icon);
mMajorContentView = (TextView) findViewById(R.id.content_major);
mMinorContentView = (TextView) findViewById(R.id.content_minor);
mTitleArea = (FrameLayout) findViewById(R.id.title_area);
@@ -184,6 +182,7 @@ public class RecordingCardView extends BaseCardView {
}
void setContent(CharSequence majorContent, CharSequence minorContent) {
+ mContentIconView.setVisibility(View.GONE);
if (!TextUtils.isEmpty(majorContent)) {
mMajorContentView.setText(majorContent);
mMajorContentView.setVisibility(View.VISIBLE);
@@ -198,6 +197,24 @@ public class RecordingCardView extends BaseCardView {
}
}
+ void setRecordingFailedContent(Context context) {
+ mContentIconView.setVisibility(View.VISIBLE);
+ mContentIconView.setImageResource(R.drawable.ic_error_outline_pink_24dp);
+ mMajorContentView.setText(context.getString(R.string.dvr_recording_failed_no_period));
+ mMajorContentView.setVisibility(View.VISIBLE);
+ mMajorContentView.setTextColor(
+ getResources().getColor(R.color.dvr_recording_failed_text_color, null));
+ }
+
+ void setRecordingConflictContent(Context context) {
+ mContentIconView.setVisibility(View.VISIBLE);
+ mContentIconView.setImageResource(R.drawable.ic_warning_yellow_24dp);
+ mMajorContentView.setText(context.getString(R.string.dvr_recording_conflict));
+ mMajorContentView.setVisibility(View.VISIBLE);
+ mMajorContentView.setTextColor(
+ getResources().getColor(R.color.dvr_recording_conflict_text_color, null));
+ }
+
/** Sets progress bar. If progress is {@code null}, hides progress bar. */
void setProgressBar(Integer progress) {
if (progress == null) {
@@ -245,19 +262,6 @@ public class RecordingCardView extends BaseCardView {
}
/**
- * Sets the affiliated icon of the card view, which will be displayed at the lower-right corner
- * of the poster.
- */
- public void setAffiliatedIcon(int imageResId) {
- if (imageResId > 0) {
- mAffiliatedIconContainer.setVisibility(View.VISIBLE);
- mAffiliatedIcon.setImageResource(imageResId);
- } else {
- mAffiliatedIconContainer.setVisibility(View.INVISIBLE);
- }
- }
-
- /**
* Sets the background image URI of the card view, which will be displayed as background when
* the view is clicked and shows its details fragment.
*/
diff --git a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
index aa2ccf75..243681c6 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
@@ -20,6 +20,7 @@ import android.os.Bundle;
import android.support.v17.leanback.app.DetailsFragment;
import com.android.tv.TvSingletons;
import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.ui.DetailsActivity;
/** {@link DetailsFragment} for recordings in DVR. */
abstract class RecordingDetailsFragment extends DvrDetailsFragment {
@@ -33,7 +34,7 @@ abstract class RecordingDetailsFragment extends DvrDetailsFragment {
@Override
protected boolean onLoadRecordingDetails(Bundle args) {
- long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID);
+ long scheduledRecordingId = args.getLong(DetailsActivity.RECORDING_ID);
mRecording =
TvSingletons.getSingletons(getContext())
.getDvrDataManager()
diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
index 302b8318..f08bb12b 100644
--- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
@@ -21,10 +21,12 @@ import android.os.Bundle;
import android.support.v17.leanback.widget.Action;
import android.support.v17.leanback.widget.OnActionClickedListener;
import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.ui.DetailsActivity;
/** {@link RecordingDetailsFragment} for scheduled recording in DVR. */
public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment {
@@ -34,14 +36,12 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment
private DvrManager mDvrManager;
private Action mScheduleAction;
private boolean mHideViewSchedule;
- private String mFailedMessage;
@Override
public void onCreate(Bundle savedInstance) {
Bundle args = getArguments();
mDvrManager = TvSingletons.getSingletons(getContext()).getDvrManager();
- mHideViewSchedule = args.getBoolean(DvrDetailsActivity.HIDE_VIEW_SCHEDULE);
- mFailedMessage = args.getString(DvrDetailsActivity.EXTRA_FAILED_MESSAGE);
+ mHideViewSchedule = args.getBoolean(DetailsActivity.HIDE_VIEW_SCHEDULE);
super.onCreate(savedInstance);
}
@@ -54,17 +54,6 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment
}
@Override
- protected void onCreateInternal() {
- if (mFailedMessage == null) {
- super.onCreateInternal();
- return;
- }
- setDetailsOverviewRow(
- DetailsContent.createFromFailedScheduledRecording(
- getContext(), getScheduledRecording(), mFailedMessage));
- }
-
- @Override
protected SparseArrayObjectAdapter onCreateActionsAdapter() {
SparseArrayObjectAdapter adapter =
new SparseArrayObjectAdapter(new ActionPresenterSelector());
diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
index 8e028689..3d279354 100644
--- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
@@ -119,21 +119,17 @@ class ScheduledRecordingPresenter extends DvrItemPresenter<ScheduledRecording> {
DetailsContent details = DetailsContent.createFromScheduledRecording(mContext, recording);
cardView.setTitle(details.getTitle());
cardView.setImageUri(details.getLogoImageUri(), details.isUsingChannelLogo());
- if (mDvrManager.isConflicting(recording)) {
- cardView.setAffiliatedIcon(R.drawable.ic_warning_white_32dp);
- } else if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
- cardView.setAffiliatedIcon(R.drawable.ic_error_white_48dp);
+ if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+ cardView.setRecordingFailedContent(mContext);
+ } else if (mDvrManager.isConflicting(recording)) {
+ cardView.setRecordingConflictContent(mContext);
} else {
- cardView.setAffiliatedIcon(0);
+ cardView.setContent(generateMajorContent(recording), null);
}
- cardView.setContent(generateMajorContent(recording), null);
cardView.setDetailBackgroundImageUri(details.getBackgroundImageUri());
}
private String generateMajorContent(ScheduledRecording recording) {
- if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
- return mContext.getString(R.string.dvr_recording_failed);
- }
int dateDifference =
Utils.computeDateDifference(System.currentTimeMillis(), recording.getStartTimeMs());
if (dateDifference <= 0) {
diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
index 2cd191a7..9104ef10 100644
--- a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
@@ -20,6 +20,7 @@ import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.media.tv.TvInputManager;
import android.os.Bundle;
+import android.support.annotation.Nullable;
import android.support.v17.leanback.app.DetailsFragment;
import android.support.v17.leanback.widget.Action;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
@@ -41,6 +42,7 @@ import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.dvr.ui.SortedArrayAdapter;
+import com.android.tv.ui.DetailsActivity;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@@ -135,7 +137,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment
@Override
protected boolean onLoadRecordingDetails(Bundle args) {
- long recordId = args.getLong(DvrDetailsActivity.RECORDING_ID);
+ long recordId = args.getLong(DetailsActivity.RECORDING_ID);
mSeries =
TvSingletons.getSingletons(getActivity())
.getDvrDataManager()
@@ -215,6 +217,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment
}
/** The programs are sorted by season number and episode number. */
+ @Nullable
private RecordedProgram getRecommendProgram(List<RecordedProgram> programs) {
for (int i = programs.size() - 1; i >= 0; i--) {
RecordedProgram program = programs.get(i);
@@ -289,7 +292,8 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment
}
}
}
- if (recordedProgram.getId() == mRecommendRecordedProgram.getId()) {
+ if (mRecommendRecordedProgram != null
+ && recordedProgram.getId() == mRecommendRecordedProgram.getId()) {
updateWatchAction();
}
}
@@ -339,14 +343,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment
new ListRow(
header,
new SeasonRowAdapter(
- selector,
- new Comparator<RecordedProgram>() {
- @Override
- public int compare(RecordedProgram lhs, RecordedProgram rhs) {
- return BaseProgram.EPISODE_COMPARATOR.compare(lhs, rhs);
- }
- },
- seasonNumber));
+ selector, BaseProgram.EPISODE_COMPARATOR::compare, seasonNumber));
getRowsAdapter().add(position, row);
return row;
}
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
index 38d3d582..11680a0d 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
@@ -37,7 +37,6 @@ import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.android.tv.R;
-import com.android.tv.TvFeatures;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.api.Channel;
@@ -90,18 +89,19 @@ class ScheduleRowPresenter extends RowPresenter {
private ScheduleRowPresenter mPresenter;
@ScheduleRowAction private int[] mActions;
private boolean mLtr;
- private LinearLayout mInfoContainer;
+ private final LinearLayout mInfoContainer;
// The first action is on the right of the second action.
- private RelativeLayout mSecondActionContainer;
- private RelativeLayout mFirstActionContainer;
- private View mSelectorView;
- private TextView mTimeView;
- private TextView mProgramTitleView;
- private TextView mInfoSeparatorView;
- private TextView mChannelNameView;
- private TextView mConflictInfoView;
- private ImageView mSecondActionView;
- private ImageView mFirstActionView;
+ private final RelativeLayout mSecondActionContainer;
+ private final RelativeLayout mFirstActionContainer;
+ private final View mSelectorView;
+ private final TextView mTimeView;
+ private final TextView mProgramTitleView;
+ private final TextView mInfoSeparatorView;
+ private final TextView mChannelNameView;
+ private final ImageView mExtraInfoIcon;
+ private final TextView mExtraInfoView;
+ private final ImageView mSecondActionView;
+ private final ImageView mFirstActionView;
private Runnable mPendingAnimationRunnable;
@@ -117,14 +117,11 @@ class ScheduleRowPresenter extends RowPresenter {
@Override
public void onFocusChange(View view, boolean focused) {
view.post(
- new Runnable() {
- @Override
- public void run() {
- if (view.isFocused()) {
- mPresenter.mLastFocusedViewId = view.getId();
- }
- updateSelector();
+ () -> {
+ if (view.isFocused()) {
+ mPresenter.mLastFocusedViewId = view.getId();
}
+ updateSelector();
});
}
};
@@ -146,7 +143,8 @@ class ScheduleRowPresenter extends RowPresenter {
mProgramTitleView = (TextView) view.findViewById(R.id.program_title);
mInfoSeparatorView = (TextView) view.findViewById(R.id.info_separator);
mChannelNameView = (TextView) view.findViewById(R.id.channel_name);
- mConflictInfoView = (TextView) view.findViewById(R.id.conflict_info);
+ mExtraInfoIcon = (ImageView) view.findViewById(R.id.extra_info_icon);
+ mExtraInfoView = (TextView) view.findViewById(R.id.extra_info);
Resources res = view.getResources();
mSelectorTranslationDelta =
res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin)
@@ -311,7 +309,7 @@ class ScheduleRowPresenter extends RowPresenter {
mInfoContainer
.getResources()
.getColor(R.color.dvr_schedules_item_info_grey, null));
- mConflictInfoView.setTextColor(
+ mExtraInfoView.setTextColor(
mInfoContainer
.getResources()
.getColor(R.color.dvr_schedules_item_info_grey, null));
@@ -327,7 +325,7 @@ class ScheduleRowPresenter extends RowPresenter {
mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null));
mChannelNameView.setTextColor(
mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null));
- mConflictInfoView.setTextColor(
+ mExtraInfoView.setTextColor(
mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null));
}
}
@@ -426,39 +424,76 @@ class ScheduleRowPresenter extends RowPresenter {
}
}
ScheduledRecording schedule = row.getSchedule();
- if (mDvrManager.isConflicting(schedule)
- || (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext())
- && schedule != null
- && schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED)) {
- String conflictInfo;
- if (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext())
- && schedule != null
- && schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
- // TODO(b/72638385): show real error messages
- // TODO(b/72638385): use a better name for ConflictInfoXXX
- conflictInfo = "Failed";
- if (schedule.getFailedReason() != null) {
- conflictInfo += " (Error code: " + schedule.getFailedReason() + ")";
- }
+ viewHolder.mExtraInfoIcon.setVisibility(View.GONE);
+ if (mDvrManager.isConflicting(schedule) || isFailedRecording(schedule)) {
+ String extraInfo;
+ if (isFailedRecording(schedule)) {
+ extraInfo =
+ mContext.getString(R.string.dvr_recording_failed_short)
+ + " "
+ + getErrorMessage(schedule);
+ viewHolder.mExtraInfoIcon.setVisibility(View.VISIBLE);
} else if (mDvrScheduleManager.isPartiallyConflicting(row.getSchedule())) {
- conflictInfo = mTunerConflictWillBePartiallyRecordedInfo;
+ extraInfo = mTunerConflictWillBePartiallyRecordedInfo;
} else {
- conflictInfo = mTunerConflictWillNotBeRecordedInfo;
+ extraInfo = mTunerConflictWillNotBeRecordedInfo;
}
- viewHolder.mConflictInfoView.setText(conflictInfo);
- viewHolder.mConflictInfoView.setVisibility(View.VISIBLE);
+ viewHolder.mExtraInfoView.setText(extraInfo);
+ viewHolder.mExtraInfoView.setVisibility(View.VISIBLE);
} else {
- viewHolder.mConflictInfoView.setVisibility(View.GONE);
+ viewHolder.mExtraInfoView.setVisibility(View.GONE);
}
if (shouldBeGrayedOut(row)) {
viewHolder.greyOutInfo();
} else {
viewHolder.whiteBackInfo();
}
+ if (isFailedRecording(schedule)) {
+ viewHolder.mExtraInfoView.setTextColor(
+ viewHolder
+ .mInfoContainer
+ .getResources()
+ .getColor(R.color.dvr_recording_failed_text_color, null));
+ }
viewHolder.mInfoContainer.setFocusable(isInfoClickable(row));
updateActionContainer(viewHolder, viewHolder.isSelected());
}
+ private boolean isFailedRecording(ScheduledRecording scheduledRecording) {
+ return scheduledRecording != null
+ && scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED;
+ }
+
+ private String getErrorMessage(ScheduledRecording recording) {
+ int reason =
+ recording.getFailedReason() == null
+ ? ScheduledRecording.FAILED_REASON_OTHER
+ : recording.getFailedReason();
+ switch (reason) {
+ case ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED:
+ return mContext.getString(R.string.dvr_recording_failed_not_started_short);
+ case ScheduledRecording.FAILED_REASON_RESOURCE_BUSY:
+ return mContext.getString(R.string.dvr_recording_failed_resource_busy_short);
+ case ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE:
+ return mContext.getString(
+ R.string.dvr_recording_failed_input_unavailable_short,
+ recording.getInputId());
+ case ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED:
+ return mContext.getString(
+ R.string.dvr_recording_failed_input_dvr_unsupported_short);
+ case ScheduledRecording.FAILED_REASON_INSUFFICIENT_SPACE:
+ return mContext.getString(R.string.dvr_recording_failed_insufficient_space_short);
+ case ScheduledRecording.FAILED_REASON_OTHER: // fall through
+ case ScheduledRecording.FAILED_REASON_NOT_FINISHED: // fall through
+ case ScheduledRecording.FAILED_REASON_SCHEDULER_STOPPED: // fall through
+ case ScheduledRecording.FAILED_REASON_INVALID_CHANNEL: // fall through
+ case ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT: // fall through
+ case ScheduledRecording.FAILED_REASON_CONNECTION_FAILED: // fall through
+ default:
+ return mContext.getString(R.string.dvr_recording_failed_system_failure, reason);
+ }
+ }
+
private int getImageForAction(@ScheduleRowAction int action) {
switch (action) {
case ACTION_START_RECORDING:
@@ -512,7 +547,8 @@ class ScheduleRowPresenter extends RowPresenter {
return schedule != null
&& (schedule.isNotStarted()
|| schedule.isInProgress()
- || schedule.isFinished());
+ || schedule.isFinished()
+ || schedule.isFailed());
}
/** Called when the button in a row is clicked. */
@@ -702,23 +738,17 @@ class ScheduleRowPresenter extends RowPresenter {
prepareShowActionView(viewHolder.mSecondActionContainer);
prepareShowActionView(viewHolder.mFirstActionContainer);
viewHolder.mPendingAnimationRunnable =
- new Runnable() {
- @Override
- public void run() {
- showActionView(viewHolder.mSecondActionContainer);
- showActionView(viewHolder.mFirstActionContainer);
- }
+ () -> {
+ showActionView(viewHolder.mSecondActionContainer);
+ showActionView(viewHolder.mFirstActionContainer);
};
break;
case 1:
prepareShowActionView(viewHolder.mFirstActionContainer);
viewHolder.mPendingAnimationRunnable =
- new Runnable() {
- @Override
- public void run() {
- hideActionView(viewHolder.mSecondActionContainer, View.GONE);
- showActionView(viewHolder.mFirstActionContainer);
- }
+ () -> {
+ hideActionView(viewHolder.mSecondActionContainer, View.GONE);
+ showActionView(viewHolder.mFirstActionContainer);
};
if (mLastFocusedViewId == R.id.action_second_container) {
mLastFocusedViewId = R.id.info_container;
@@ -727,12 +757,9 @@ class ScheduleRowPresenter extends RowPresenter {
case 0:
default:
viewHolder.mPendingAnimationRunnable =
- new Runnable() {
- @Override
- public void run() {
- hideActionView(viewHolder.mSecondActionContainer, View.GONE);
- hideActionView(viewHolder.mFirstActionContainer, View.GONE);
- }
+ () -> {
+ hideActionView(viewHolder.mSecondActionContainer, View.GONE);
+ hideActionView(viewHolder.mFirstActionContainer, View.GONE);
};
mLastFocusedViewId = R.id.info_container;
SoftPreconditions.checkState(
diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
index eb01aba2..28a44bf3 100644
--- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
@@ -211,13 +211,7 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter {
new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean focused) {
- view.post(
- new Runnable() {
- @Override
- public void run() {
- updateSelector(view);
- }
- });
+ view.post(() -> updateSelector(view));
}
};
mSeriesSettingsButton.setOnFocusChangeListener(onFocusChangeListener);
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
index b8b19adc..f24ad2c0 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
@@ -74,8 +74,10 @@ public class DvrPlaybackActivity extends Activity implements OnPinCheckedListene
private Intent createProgramIntent(Intent intent) {
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
- long recordedProgramId = ContentUris.parseId(uri);
- intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, recordedProgramId);
+ if (uri != null) {
+ long recordedProgramId = ContentUris.parseId(uri);
+ intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, recordedProgramId);
+ }
}
return intent;
}
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java
index 59c90d11..791d26bb 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java
@@ -39,6 +39,7 @@ import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
+import android.view.ViewGroup;
import com.android.tv.R;
import com.android.tv.util.TimeShiftUtils;
import java.util.ArrayList;
@@ -53,10 +54,13 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue {
private static final boolean DEBUG = false;
private static final int AUDIO_ACTION_ID = 1001;
+ private static final long INVALID_TIME = -1;
private int mPlaybackState = PlaybackState.STATE_NONE;
private int mPlaybackSpeedLevel;
private int mPlaybackSpeedId;
+ private long mProgramStartTimeMs = INVALID_TIME;
+ private boolean mEnableBuffering = false;
private boolean mReadyToControl;
private final DvrPlaybackOverlayFragment mFragment;
@@ -67,6 +71,8 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue {
private final MultiAction mClosedCaptioningAction;
private final MultiAction mMultiAudioAction;
private ArrayObjectAdapter mSecondaryActionsAdapter;
+ private PlaybackControlsRow mPlaybackControlsRow;
+ @Nullable private View mPlayPauseButton;
DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) {
super(activity, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]);
@@ -79,13 +85,18 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue {
.getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top);
mClosedCaptioningAction = new ClosedCaptioningAction(activity);
mMultiAudioAction = new MultiAudioAction(activity);
+ mProgramStartTimeMs = overlayFragment.getProgramStartTimeMs();
+ if (mProgramStartTimeMs != INVALID_TIME) {
+ mEnableBuffering = true;
+ }
createControlsRowPresenter();
}
void createControlsRow() {
- PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
- setControlsRow(controlsRow);
- mSecondaryActionsAdapter = (ArrayObjectAdapter) controlsRow.getSecondaryActionsAdapter();
+ mPlaybackControlsRow = new PlaybackControlsRow(this);
+ setControlsRow(mPlaybackControlsRow);
+ mSecondaryActionsAdapter =
+ (ArrayObjectAdapter) mPlaybackControlsRow.getSecondaryActionsAdapter();
}
private void createControlsRowPresenter() {
@@ -118,6 +129,8 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue {
protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
super.onBindRowViewHolder(vh, item);
vh.setOnKeyListener(DvrPlaybackControlHelper.this);
+ ViewGroup controlBar = (ViewGroup) vh.view.findViewById(R.id.control_bar);
+ mPlayPauseButton = controlBar.getChildAt(1);
}
@Override
@@ -265,6 +278,13 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue {
getHost().notifyPlaybackRowChanged();
}
+ /** Update the focus to play pause button. */
+ public void onPlaybackResume() {
+ if (mPlayPauseButton != null) {
+ mPlayPauseButton.requestFocus();
+ }
+ }
+
@Nullable
Boolean hasSecondaryRow() {
if (mSecondaryActionsAdapter == null) {
@@ -292,6 +312,15 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue {
mTransportControls.pause();
}
+ @Override
+ public void updateProgress() {
+ if (mEnableBuffering) {
+ super.updateProgress();
+ long bufferedTimeMs = System.currentTimeMillis() - mProgramStartTimeMs;
+ mPlaybackControlsRow.setBufferedPosition(bufferedTimeMs);
+ }
+ }
+
/** Notifies closed caption being enabled/disabled to update related UI. */
void onSubtitleTrackStateChanged(boolean enabled) {
mClosedCaptioningAction.setIndex(
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java
index bef036eb..81abb8e4 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java
@@ -39,9 +39,6 @@ import com.android.tv.util.Utils;
import com.android.tv.util.images.ImageLoader;
class DvrPlaybackMediaSessionHelper {
- private static final String TAG = "DvrPlaybackMediaSessionHelper";
- private static final boolean DEBUG = false;
-
private int mNowPlayingCardWidth;
private int mNowPlayingCardHeight;
private int mSpeedLevel;
@@ -73,6 +70,9 @@ class DvrPlaybackMediaSessionHelper {
@Override
public void onPlaybackPositionChanged(long positionMs) {
updateMediaSessionPlaybackState();
+ if (getProgram().isPartial()) {
+ overlayFragment.updateProgress();
+ }
if (mDvrPlayer.isPlaybackPrepared()) {
mDvrWatchedPositionManager.setWatchedPosition(
mDvrPlayer.getProgram().getId(), positionMs);
@@ -94,6 +94,11 @@ class DvrPlaybackMediaSessionHelper {
mActivity.startActivity(intent);
}
}
+
+ @Override
+ public void onPlaybackResume() {
+ overlayFragment.onPlaybackResume();
+ }
});
initializeMediaSession(mediaSessionTag);
}
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
index d3374cfa..1059e852 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
@@ -25,7 +25,6 @@ import android.media.session.PlaybackState;
import android.media.tv.TvContentRating;
import android.media.tv.TvInputManager;
import android.media.tv.TvTrackInfo;
-import android.media.tv.TvView;
import android.os.Bundle;
import android.support.v17.leanback.app.PlaybackFragment;
import android.support.v17.leanback.app.PlaybackFragmentGlueHost;
@@ -52,7 +51,7 @@ import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.ui.SortedArrayAdapter;
import com.android.tv.dvr.ui.browse.DvrListRowPresenter;
import com.android.tv.dvr.ui.browse.RecordingCardView;
-import com.android.tv.parental.ContentRatingsManager;
+import com.android.tv.ui.AppLayerTvView;
import com.android.tv.util.TvSettings;
import com.android.tv.util.TvTrackInfoUtils;
import com.android.tv.util.Utils;
@@ -66,6 +65,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment {
private static final String MEDIA_SESSION_TAG = "com.android.tv.dvr.mediasession";
private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f;
+ private static final long INVALID_TIME = -1;
// mProgram is only used to store program from intent. Don't use it elsewhere.
private RecordedProgram mProgram;
@@ -76,8 +76,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment {
private SortedArrayAdapter<BaseProgram> mRelatedRecordingsRowAdapter;
private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter;
private DvrDataManager mDvrDataManager;
- private ContentRatingsManager mContentRatingsManager;
- private TvView mTvView;
+ private AppLayerTvView mTvView;
private View mBlockScreenView;
private ListRow mRelatedRecordingsRow;
private int mVerticalPaddingBase;
@@ -117,10 +116,6 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment {
.getDimensionPixelOffset(
R.dimen.dvr_playback_overlay_padding_top_no_secondary_row);
mDvrDataManager = TvSingletons.getSingletons(getActivity()).getDvrDataManager();
- mContentRatingsManager =
- TvSingletons.getSingletons(getContext())
- .getTvInputManagerHelper()
- .getContentRatingsManager();
if (!mDvrDataManager.isRecordedProgramLoadFinished()) {
mDvrDataManager.addRecordedProgramLoadFinishedListener(
new DvrDataManager.OnRecordedProgramLoadFinishedListener() {
@@ -157,9 +152,9 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment {
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
- mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view);
+ mTvView = getActivity().findViewById(R.id.dvr_tv_view);
mBlockScreenView = getActivity().findViewById(R.id.block_screen);
- mDvrPlayer = new DvrPlayer(mTvView);
+ mDvrPlayer = new DvrPlayer(mTvView, getActivity());
mMediaSessionHelper =
new DvrPlaybackMediaSessionHelper(
getActivity(), MEDIA_SESSION_TAG, mDvrPlayer, this);
@@ -279,6 +274,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment {
mPlaybackControlHelper.unregisterCallback();
mMediaSessionHelper.release();
mRelatedRecordingCardPresenter.unbindAllViewHolders();
+ mDvrPlayer.release();
super.onDestroy();
}
@@ -503,6 +499,20 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment {
}
}
+ public void onPlaybackResume() {
+ mPlaybackControlHelper.onPlaybackResume();
+ }
+
+ public long getProgramStartTimeMs() {
+ return (mProgram != null && mProgram.isPartial())
+ ? mProgram.getStartTimeUtcMillis()
+ : INVALID_TIME;
+ }
+
+ public void updateProgress() {
+ mPlaybackControlHelper.updateProgress();
+ }
+
private class RelatedRecordingsAdapter extends SortedArrayAdapter<BaseProgram> {
RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) {
super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR);
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
index 85bb31b2..d14646b8 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
@@ -16,6 +16,7 @@
package com.android.tv.dvr.ui.playback;
+import android.content.Context;
import android.media.PlaybackParams;
import android.media.session.PlaybackState;
import android.media.tv.TvContentRating;
@@ -24,12 +25,16 @@ import android.media.tv.TvTrackInfo;
import android.media.tv.TvView;
import android.text.TextUtils;
import android.util.Log;
+import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat;
+import com.android.tv.dvr.DvrTvView;
import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.ui.AppLayerTvView;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
-class DvrPlayer {
+/** Player for recorded programs. */
+public class DvrPlayer {
private static final String TAG = "DvrPlayer";
private static final boolean DEBUG = false;
@@ -40,10 +45,11 @@ class DvrPlayer {
private static final long SEEK_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(2);
private static final long REWIND_POSITION_MARGIN_MS = 32; // Workaround value. b/29994826
+ private static final long FORWARD_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(5);
private RecordedProgram mProgram;
private long mInitialSeekPositionMs;
- private final TvView mTvView;
+ private final DvrTvView mTvView;
private DvrPlayerCallback mCallback;
private OnAspectRatioChangedListener mOnAspectRatioChangedListener;
private OnContentBlockedListener mOnContentBlockedListener;
@@ -63,6 +69,7 @@ class DvrPlayer {
private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
private boolean mTimeShiftPlayAvailable;
+ /** Callback of DVR player. */
public static class DvrPlayerCallback {
/**
* Called when the playback position is changed. The normal updating frequency is around 1
@@ -74,8 +81,11 @@ class DvrPlayer {
public void onPlaybackStateChanged(int playbackState, int playbackSpeed) {}
/** Called when the playback toward the end. */
public void onPlaybackEnded() {}
+ /** Called when the playback is resumed to live position. */
+ public void onPlaybackResume() {}
}
+ /** Listener for aspect ratio changed events. */
public interface OnAspectRatioChangedListener {
/**
* Called when the Video's aspect ratio is changed.
@@ -86,27 +96,32 @@ class DvrPlayer {
void onAspectRatioChanged(float videoAspectRatio);
}
+ /** Listener for content blocked events. */
public interface OnContentBlockedListener {
/** Called when the Video's aspect ratio is changed. */
void onContentBlocked(TvContentRating rating);
}
+ /** Listener for tracks availability changed events */
public interface OnTracksAvailabilityChangedListener {
/** Called when the Video's subtitle or audio tracks are changed. */
void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio);
}
+ /** Listener for track selected events */
public interface OnTrackSelectedListener {
/** Called when certain subtitle or audio track is selected. */
void onTrackSelected(String selectedTrackId);
}
- public DvrPlayer(TvView tvView) {
- mTvView = tvView;
+ /** Constructor of DvrPlayer. */
+ public DvrPlayer(AppLayerTvView tvView, Context context) {
+ mTvView = new DvrTvView(context, tvView, this);
mTvView.setCaptionEnabled(true);
mPlaybackParams.setSpeed(1.0f);
setTvViewCallbacks();
setCallback(null);
+ mTvView.init();
}
/**
@@ -333,7 +348,8 @@ class DvrPlayer {
/** Returns the audio tracks of the current playback. */
public ArrayList<TvTrackInfo> getAudioTracks() {
- return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_AUDIO));
+ List<TvTrackInfo> tracks = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO);
+ return tracks == null ? new ArrayList<>() : new ArrayList<>(tracks);
}
/** Returns the ID of the selected track of the given type. */
@@ -352,6 +368,10 @@ class DvrPlayer {
&& mPlaybackState != PlaybackState.STATE_CONNECTING;
}
+ public void release() {
+ mTvView.release();
+ }
+
/**
* Selects the given track.
*
@@ -426,9 +446,16 @@ class DvrPlayer {
resumeToWatchedPositionIfNeeded();
}
timeMs -= mStartPositionMs;
- if (mPlaybackState == PlaybackState.STATE_REWINDING
- && timeMs <= REWIND_POSITION_MARGIN_MS) {
+ long bufferedTimeMs =
+ System.currentTimeMillis()
+ - mProgram.getStartTimeUtcMillis()
+ - FORWARD_POSITION_MARGIN_MS;
+ if ((mPlaybackState == PlaybackState.STATE_REWINDING
+ && timeMs <= REWIND_POSITION_MARGIN_MS)
+ || (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING
+ && timeMs > bufferedTimeMs)) {
play();
+ mCallback.onPlaybackResume();
} else {
mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0);
mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs);
@@ -440,7 +467,7 @@ class DvrPlayer {
}
});
mTvView.setCallback(
- new TvView.TvInputCallback() {
+ new TvInputCallbackCompat() {
@Override
public void onTimeShiftStatusChanged(String inputId, int status) {
if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status);
diff --git a/src/com/android/tv/features/PartnerFeatures.java b/src/com/android/tv/features/PartnerFeatures.java
new file mode 100644
index 00000000..6d680b7b
--- /dev/null
+++ b/src/com/android/tv/features/PartnerFeatures.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018 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.features;
+
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import com.android.tv.common.feature.Feature;
+import com.google.android.tv.partner.support.PartnerCustomizations;
+
+/** Features backed by {@link PartnerCustomizations}. */
+@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
+public final class PartnerFeatures {
+
+ public static final Feature TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE =
+ new PartnerFeature(
+ PartnerCustomizations.TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE);
+
+ public static final Feature TURN_OFF_EMBEDDED_TUNER =
+ new PartnerFeature(PartnerCustomizations.TURN_OFF_EMBEDDED_TUNER);
+
+ public static final Feature TVPROVIDER_ALLOWS_COLUMN_CREATION =
+ new PartnerFeature(PartnerCustomizations.TVPROVIDER_ALLOWS_COLUMN_CREATION);
+
+ private static class PartnerFeature implements Feature {
+
+ private final String property;
+
+ public PartnerFeature(String property) {
+ this.property = property;
+ }
+
+ @Override
+ public boolean isEnabled(Context context) {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ PartnerCustomizations partnerCustomizations = new PartnerCustomizations(context);
+ return partnerCustomizations.getBooleanResource(context, property).orElse(false);
+ }
+ return false;
+ }
+ }
+
+ private PartnerFeatures() {}
+}
diff --git a/src/com/android/tv/TvFeatures.java b/src/com/android/tv/features/TvFeatures.java
index d2cf76e7..208d53f6 100644
--- a/src/com/android/tv/TvFeatures.java
+++ b/src/com/android/tv/features/TvFeatures.java
@@ -14,13 +14,15 @@
* limitations under the License
*/
-package com.android.tv;
+package com.android.tv.features;
-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.BuildTypeFeature.ASOP_FEATURE;
+import static com.android.tv.common.feature.BuildTypeFeature.ENG_ONLY_FEATURE;
import static com.android.tv.common.feature.FeatureUtils.OFF;
import static com.android.tv.common.feature.FeatureUtils.ON;
-import static com.android.tv.common.feature.FeatureUtils.OR;
+import static com.android.tv.common.feature.FeatureUtils.and;
+import static com.android.tv.common.feature.FeatureUtils.not;
+import static com.android.tv.common.feature.FeatureUtils.or;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -31,14 +33,14 @@ import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.feature.ExperimentFeature;
import com.android.tv.common.feature.Feature;
import com.android.tv.common.feature.FeatureUtils;
-import com.android.tv.common.feature.GServiceFeature;
+import com.android.tv.common.feature.FlagFeature;
import com.android.tv.common.feature.PropertyFeature;
import com.android.tv.common.feature.Sdk;
import com.android.tv.common.feature.TestableFeature;
+import com.android.tv.common.flags.has.HasUiFlags;
+import com.android.tv.common.singletons.HasSingletons;
import com.android.tv.common.util.PermissionUtils;
-import com.google.android.tv.partner.support.PartnerCustomizations;
-
/**
* List of {@link Feature} for the Live TV App.
*
@@ -46,33 +48,48 @@ import com.google.android.tv.partner.support.PartnerCustomizations;
*/
public final class TvFeatures extends CommonFeatures {
+ /** When enabled store network affiliation information to TV provider */
+ public static final Feature STORE_NETWORK_AFFILIATION = ENG_ONLY_FEATURE;
+
/** When enabled use system setting for turning on analytics. */
public static final Feature ANALYTICS_OPT_IN =
ExperimentFeature.from(Experiments.ENABLE_ANALYTICS_VIA_CHECKBOX);
- /** When enabled shows a list of failed recordings */
- public static final Feature DVR_FAILED_LIST = 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 final Feature ANALYTICS_V2 = AND(ON, ANALYTICS_OPT_IN);
+ public static final Feature ANALYTICS_V2 = and(ON, ANALYTICS_OPT_IN);
+
+ private static final Feature TV_PROVIDER_ALLOWS_INSERT_TO_PROGRAM_TABLE =
+ or(Sdk.AT_LEAST_O, PartnerFeatures.TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE);
+
+ /**
+ * Enable cloud EPG for third parties.
+ *
+ * @see <a href="http://go/cloud-epg-3p-proposal">go/cloud-epg-3p-proposal</a>
+ */
+ // TODO verify customization for N
+ public static final TestableFeature CLOUD_EPG_FOR_3RD_PARTY =
+ TestableFeature.createTestableFeature(
+ and(
+ not(ASOP_FEATURE),
+ // TODO(b/66696290): use newer version of robolectric.
+ or(
+ TV_PROVIDER_ALLOWS_INSERT_TO_PROGRAM_TABLE,
+ FeatureUtils.ROBOLECTRIC)));
- public static final Feature EPG_SEARCH =
- PropertyFeature.create("feature_tv_use_epg_search", false);
+ // TODO(b/76149661): Fix EPG search or remove it
+ public static final Feature EPG_SEARCH = OFF;
- 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 final Feature UNHIDE =
- 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);
- }
- });
+ or(
+ FlagFeature.from(
+ context -> HasSingletons.get(HasUiFlags.class, context),
+ input -> input.getUiFlags().uhideLauncher()),
+ // If LC app runs as non-system app, we unhide the app.
+ not(PermissionUtils::hasAccessAllEpg));
public static final Feature PICTURE_IN_PICTURE =
new Feature() {
diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java
index 5b53f904..bc1b11b6 100644
--- a/src/com/android/tv/guide/ProgramGuide.java
+++ b/src/com/android/tv/guide/ProgramGuide.java
@@ -47,7 +47,7 @@ import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeL
import com.android.tv.ChannelTuner;
import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.TvFeatures;
+import com.android.tv.TvSingletons;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.WeakHandler;
import com.android.tv.common.util.DurationTimer;
@@ -56,11 +56,16 @@ import com.android.tv.data.GenreItems;
import com.android.tv.data.ProgramDataManager;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrScheduleManager;
+import com.android.tv.features.TvFeatures;
+import com.android.tv.perf.EventNames;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.TimerEvent;
import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter;
import com.android.tv.ui.ViewUtils;
import com.android.tv.ui.hideable.AutoHideScheduler;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
+import com.android.tv.common.flags.BackendKnobsFlags;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -150,6 +155,9 @@ public class ProgramGuide
private final ProgramManagerListener mProgramManagerListener = new ProgramManagerListener();
+ private final PerformanceMonitor mPerformanceMonitor;
+ private TimerEvent mTimerEvent;
+
private final Runnable mUpdateTimeIndicator =
new Runnable() {
@Override
@@ -175,13 +183,17 @@ public class ProgramGuide
Runnable preShowRunnable,
Runnable postHideRunnable) {
mActivity = activity;
+ TvSingletons singletons = TvSingletons.getSingletons(mActivity);
+ mPerformanceMonitor = singletons.getPerformanceMonitor();
+ BackendKnobsFlags backendKnobsFlags = singletons.getBackendKnobs();
mProgramManager =
new ProgramManager(
tvInputManagerHelper,
channelDataManager,
programDataManager,
dvrDataManager,
- dvrScheduleManager);
+ dvrScheduleManager,
+ backendKnobsFlags);
mChannelTuner = channelTuner;
mTracker = tracker;
mPreShowRunnable = preShowRunnable;
@@ -316,12 +328,43 @@ public class ProgramGuide
mGrid.setItemAlignmentOffset(0);
mGrid.setItemAlignmentOffsetPercent(ProgramGrid.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
+ mGrid.addOnScrollListener(
+ new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ if (DEBUG) {
+ Log.d(TAG, "ProgramGrid onScrollStateChanged. newState=" + newState);
+ }
+ if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
+ mPerformanceMonitor.startJankRecorder(
+ EventNames.PROGRAM_GUIDE_SCROLL_VERTICALLY);
+ } else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+ mPerformanceMonitor.stopJankRecorder(
+ EventNames.PROGRAM_GUIDE_SCROLL_VERTICALLY);
+ }
+ }
+ });
+
RecyclerView.OnScrollListener onScrollListener =
new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
onHorizontalScrolled(dx);
}
+
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ if (DEBUG) {
+ Log.d(TAG, "TimelineRow onScrollStateChanged. newState=" + newState);
+ }
+ if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
+ mPerformanceMonitor.startJankRecorder(
+ EventNames.PROGRAM_GUIDE_SCROLL_HORIZONTALLY);
+ } else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+ mPerformanceMonitor.stopJankRecorder(
+ EventNames.PROGRAM_GUIDE_SCROLL_HORIZONTALLY);
+ }
+ }
};
mTimelineRow.addOnScrollListener(onScrollListener);
@@ -332,6 +375,18 @@ public class ProgramGuide
R.animator.program_guide_side_panel_enter_full,
0,
R.animator.program_guide_table_enter_full);
+ mShowAnimatorFull.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mTimerEvent != null) {
+ mPerformanceMonitor.stopTimer(
+ mTimerEvent, EventNames.PROGRAM_GUIDE_SHOW);
+ mTimerEvent = null;
+ }
+ mPerformanceMonitor.stopJankRecorder(EventNames.PROGRAM_GUIDE_SHOW);
+ }
+ });
mShowAnimatorPartial =
createAnimator(
@@ -345,6 +400,16 @@ public class ProgramGuide
mSidePanelGridView.setVisibility(View.VISIBLE);
mSidePanelGridView.setAlpha(1.0f);
}
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mTimerEvent != null) {
+ mPerformanceMonitor.stopTimer(
+ mTimerEvent, EventNames.PROGRAM_GUIDE_SHOW);
+ mTimerEvent = null;
+ }
+ mPerformanceMonitor.stopJankRecorder(EventNames.PROGRAM_GUIDE_SHOW);
+ }
});
mHideAnimatorFull =
@@ -355,6 +420,11 @@ public class ProgramGuide
mHideAnimatorFull.addListener(
new AnimatorListenerAdapter() {
@Override
+ public void onAnimationStart(Animator animation) {
+ mPerformanceMonitor.recordMemory(EventNames.MEMORY_ON_PROGRAM_GUIDE_CLOSE);
+ }
+
+ @Override
public void onAnimationEnd(Animator animation) {
mContainer.setVisibility(View.GONE);
}
@@ -367,6 +437,11 @@ public class ProgramGuide
mHideAnimatorPartial.addListener(
new AnimatorListenerAdapter() {
@Override
+ public void onAnimationStart(Animator animation) {
+ mPerformanceMonitor.recordMemory(EventNames.MEMORY_ON_PROGRAM_GUIDE_CLOSE);
+ }
+
+ @Override
public void onAnimationEnd(Animator animation) {
mContainer.setVisibility(View.GONE);
}
@@ -447,6 +522,8 @@ public class ProgramGuide
if (mContainer.getVisibility() == View.VISIBLE) {
return;
}
+ mTimerEvent = mPerformanceMonitor.startTimer();
+ mPerformanceMonitor.startJankRecorder(EventNames.PROGRAM_GUIDE_SHOW);
mTracker.sendShowEpg();
mTracker.sendScreenView(SCREEN_NAME);
if (mPreShowRunnable != null) {
@@ -643,6 +720,11 @@ public class ProgramGuide
return mGrid;
}
+ /** Returns if Accessibility is enabled. */
+ boolean isAccessibilityEnabled() {
+ return mAccessibilityManager.isEnabled();
+ }
+
/** Gets {@link VerticalGridView} for "genre select" side panel. */
VerticalGridView getSidePanel() {
return mSidePanelGridView;
@@ -711,9 +793,7 @@ public class ProgramGuide
}
private void startFull() {
- if (!mShowGuidePartial || mAccessibilityManager.isEnabled()) {
- // If accessibility service is enabled, focus cannot be moved to side panel due to it's
- // hidden. Therefore, we don't hide side panel when accessibility service is enabled.
+ if (!mShowGuidePartial) {
return;
}
mShowGuidePartial = false;
@@ -806,13 +886,7 @@ public class ProgramGuide
detailView.setVisibility(View.VISIBLE);
final ProgramRow programRow = (ProgramRow) row.findViewById(R.id.row);
- programRow.post(
- new Runnable() {
- @Override
- public void run() {
- programRow.focusCurrentProgram();
- }
- });
+ programRow.post(programRow::focusCurrentProgram);
} else {
animateRowChange(mSelectedRow, row);
}
@@ -935,6 +1009,7 @@ public class ProgramGuide
private static final int UNKNOWN = 0;
private static final int SIDE_PANEL = 1;
private static final int PROGRAM_TABLE = 2;
+ private static final int CHANNEL_COLUMN = 3;
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
@@ -948,6 +1023,10 @@ public class ProgramGuide
startFull();
} else if (fromLocation == PROGRAM_TABLE && toLocation == SIDE_PANEL) {
startPartial();
+ } else if (fromLocation == CHANNEL_COLUMN && toLocation == PROGRAM_TABLE) {
+ startFull();
+ } else if (fromLocation == PROGRAM_TABLE && toLocation == CHANNEL_COLUMN) {
+ startPartial();
}
}
@@ -959,7 +1038,11 @@ public class ProgramGuide
if (obj == mSidePanel) {
return SIDE_PANEL;
} else if (obj == mGrid) {
- return PROGRAM_TABLE;
+ if (view instanceof ProgramItemView) {
+ return PROGRAM_TABLE;
+ } else {
+ return CHANNEL_COLUMN;
+ }
}
}
return UNKNOWN;
diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java
index 9f379e43..a46beab7 100644
--- a/src/com/android/tv/guide/ProgramItemView.java
+++ b/src/com/android/tv/guide/ProgramItemView.java
@@ -103,12 +103,9 @@ public class ProgramItemView extends TextView {
tvActivity.getChannelDataManager().getChannel(entry.channelId);
if (entry.isCurrentProgram()) {
view.postDelayed(
- new Runnable() {
- @Override
- public void run() {
- tvActivity.tuneToChannel(channel);
- tvActivity.hideOverlaysForTune();
- }
+ () -> {
+ tvActivity.tuneToChannel(channel);
+ tvActivity.hideOverlaysForTune();
},
entry.getWidth() > ((ProgramItemView) view).mMaxWidthForRipple
? 0
@@ -125,13 +122,9 @@ public class ProgramItemView extends TextView {
DvrUiHelper.checkStorageStatusAndShowErrorMessage(
tvActivity,
channel.getInputId(),
- new Runnable() {
- @Override
- public void run() {
+ () ->
DvrUiHelper.requestRecordingFutureProgram(
- tvActivity, entry.program, false);
- }
- });
+ tvActivity, entry.program, false));
} else {
dvrManager.removeScheduledRecording(entry.scheduledRecording);
String msg =
@@ -378,7 +371,7 @@ public class ProgramItemView extends TextView {
int iconResId = 0;
if (isEntryWideEnough() && mTableEntry.scheduledRecording != null) {
if (mDvrManager.isConflicting(mTableEntry.scheduledRecording)) {
- iconResId = R.drawable.ic_warning_white_18dp;
+ iconResId = R.drawable.quantum_ic_warning_white_18;
} else {
switch (mTableEntry.scheduledRecording.getState()) {
case ScheduledRecording.STATE_RECORDING_NOT_STARTED:
@@ -405,20 +398,22 @@ public class ProgramItemView extends TextView {
if (channel != null) {
description = channel.getDisplayNumber() + " " + description;
}
- description +=
- " "
- + Utils.getDurationString(
- getContext(),
- mClock,
- mTableEntry.entryStartUtcMillis,
- mTableEntry.entryEndUtcMillis,
- true);
Program program = mTableEntry.program;
if (program != null) {
+ description += " " + program.getDurationString(getContext());
String episodeDescription = program.getEpisodeContentDescription(getContext());
if (!TextUtils.isEmpty(episodeDescription)) {
description += " " + episodeDescription;
}
+ } else {
+ description +=
+ " "
+ + Utils.getDurationString(
+ getContext(),
+ mClock,
+ mTableEntry.entryStartUtcMillis,
+ mTableEntry.entryEndUtcMillis,
+ true);
}
if (mTableEntry.scheduledRecording != null) {
if (mDvrManager.isConflicting(mTableEntry.scheduledRecording)) {
diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java
index 3f20a837..3a5a4a02 100644
--- a/src/com/android/tv/guide/ProgramManager.java
+++ b/src/com/android/tv/guide/ProgramManager.java
@@ -32,6 +32,7 @@ import com.android.tv.dvr.DvrScheduleManager.OnConflictStateChangeListener;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
+import com.android.tv.common.flags.BackendKnobsFlags;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -59,6 +60,7 @@ public class ProgramManager {
private final ProgramDataManager mProgramDataManager;
private final DvrDataManager mDvrDataManager; // Only set if DVR is enabled
private final DvrScheduleManager mDvrScheduleManager;
+ private final BackendKnobsFlags mBackendKnobsFlags;
private long mStartUtcMillis;
private long mEndUtcMillis;
@@ -114,12 +116,26 @@ public class ProgramManager {
}
};
- private final ProgramDataManager.Listener mProgramDataManagerListener =
- new ProgramDataManager.Listener() {
+ private final ProgramDataManager.Callback mProgramDataManagerCallback =
+ new ProgramDataManager.Callback() {
@Override
public void onProgramUpdated() {
updateTableEntries(true);
}
+
+ @Override
+ public void onSingleChannelUpdated(long channelId) {
+ boolean parentalControlsEnabled =
+ mTvInputManagerHelper
+ .getParentalControlSettings()
+ .isParentalControlsEnabled();
+ // Inline the updating of the mChannelIdEntriesMap here so we can only call
+ // getParentalControlSettings once.
+ List<TableEntry> entries =
+ createProgramEntries(channelId, parentalControlsEnabled);
+ mChannelIdEntriesMap.put(channelId, entries);
+ notifyTableEntriesUpdated();
+ }
};
private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener =
@@ -199,19 +215,21 @@ public class ProgramManager {
ChannelDataManager channelDataManager,
ProgramDataManager programDataManager,
@Nullable DvrDataManager dvrDataManager,
- @Nullable DvrScheduleManager dvrScheduleManager) {
+ @Nullable DvrScheduleManager dvrScheduleManager,
+ BackendKnobsFlags backendKnobsFlags) {
mTvInputManagerHelper = tvInputManagerHelper;
mChannelDataManager = channelDataManager;
mProgramDataManager = programDataManager;
mDvrDataManager = dvrDataManager;
mDvrScheduleManager = dvrScheduleManager;
+ mBackendKnobsFlags = backendKnobsFlags;
}
void programGuideVisibilityChanged(boolean visible) {
mProgramDataManager.setPauseProgramUpdate(visible);
if (visible) {
mChannelDataManager.addListener(mChannelDataManagerListener);
- mProgramDataManager.addListener(mProgramDataManagerListener);
+ mProgramDataManager.addCallback(mProgramDataManagerCallback);
if (mDvrDataManager != null) {
if (!mDvrDataManager.isDvrScheduleLoadFinished()) {
mDvrDataManager.addDvrScheduleLoadFinishedListener(mDvrLoadedListener);
@@ -224,7 +242,7 @@ public class ProgramManager {
}
} else {
mChannelDataManager.removeListener(mChannelDataManagerListener);
- mProgramDataManager.removeListener(mProgramDataManagerListener);
+ mProgramDataManager.removeCallback(mProgramDataManagerCallback);
if (mDvrDataManager != null) {
mDvrDataManager.removeDvrScheduleLoadFinishedListener(mDvrLoadedListener);
mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener);
@@ -233,6 +251,7 @@ public class ProgramManager {
mDvrScheduleManager.removeOnConflictStateChangeListener(
mOnConflictStateChangeListener);
}
+ mChannelIdEntriesMap.clear();
}
}
@@ -309,8 +328,8 @@ public class ProgramManager {
long fromUtcMillis = mFromUtcMillis + timeMillisToScroll;
long toUtcMillis = mToUtcMillis + timeMillisToScroll;
if (fromUtcMillis < mStartUtcMillis) {
- fromUtcMillis = mStartUtcMillis;
toUtcMillis += mStartUtcMillis - fromUtcMillis;
+ fromUtcMillis = mStartUtcMillis;
}
if (toUtcMillis > mEndUtcMillis) {
fromUtcMillis -= toUtcMillis - mEndUtcMillis;
@@ -345,10 +364,12 @@ public class ProgramManager {
/** Returns the program index of the program at {@code time} or -1 if not found. */
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);
- if (entry.entryStartUtcMillis <= time && time < entry.entryEndUtcMillis) {
- return i;
+ if (entries != null) {
+ for (int i = 0; i < entries.size(); ++i) {
+ TableEntry entry = entries.get(i);
+ if (entry.entryStartUtcMillis <= time && time < entry.entryEndUtcMillis) {
+ return i;
+ }
}
}
return -1;
@@ -401,7 +422,7 @@ public class ProgramManager {
* given {@code channelId}.
*/
int getTableEntryCount(long channelId) {
- return mChannelIdEntriesMap.get(channelId).size();
+ return mChannelIdEntriesMap.isEmpty() ? 0 : mChannelIdEntriesMap.get(channelId).size();
}
/**
@@ -410,6 +431,9 @@ public class ProgramManager {
* (e.g., whose channelId is INVALID_ID), when it corresponds to a gap between programs.
*/
TableEntry getTableEntry(long channelId, int index) {
+ if (mBackendKnobsFlags.enablePartialProgramFetch()) {
+ mProgramDataManager.prefetchChannel(channelId);
+ }
return mChannelIdEntriesMap.get(channelId).get(index);
}
@@ -437,6 +461,14 @@ public class ProgramManager {
buildGenreFilters();
}
+ /** Sets the channel list for testing */
+ void setChannels(List<Channel> channels) {
+ mChannels = new ArrayList<>(channels);
+ mSelectedGenreId = GenreItems.ID_ALL_CHANNELS;
+ mFilteredChannels = mChannels;
+ buildGenreFilters();
+ }
+
private void updateTableEntries(boolean clear) {
updateTableEntriesWithoutNotification(clear);
notifyTableEntriesUpdated();
@@ -544,6 +576,9 @@ public class ProgramManager {
@Nullable
private TableEntry getTableEntry(long channelId, long entryId) {
+ if (mChannelIdEntriesMap.isEmpty()) {
+ return null;
+ }
List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
if (entries != null) {
for (TableEntry entry : entries) {
diff --git a/src/com/android/tv/guide/ProgramRow.java b/src/com/android/tv/guide/ProgramRow.java
index 83175bb6..3317c15f 100644
--- a/src/com/android/tv/guide/ProgramRow.java
+++ b/src/com/android/tv/guide/ProgramRow.java
@@ -72,6 +72,9 @@ public class ProgramRow extends TimelineGridView {
public ProgramRow(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+ ProgramRowAccessibilityDelegate rowAccessibilityDelegate =
+ new ProgramRowAccessibilityDelegate(this);
+ this.setAccessibilityDelegateCompat(rowAccessibilityDelegate);
}
/** Registers a listener focus events occurring on children to the {@code ProgramRow}. */
@@ -126,13 +129,26 @@ public class ProgramRow extends TimelineGridView {
: direction == View.FOCUS_LEFT;
}
+ // When Accessibility is enabled, this API will keep next node visible
+ void focusSearchAccessibility(View focused, int direction) {
+ TableEntry focusedEntry = ((ProgramItemView) focused).getTableEntry();
+ long toMillis = mProgramManager.getToUtcMillis();
+
+ if (isDirectionEnd(direction) || direction == View.FOCUS_FORWARD) {
+ if (focusedEntry.entryEndUtcMillis >= toMillis) {
+ scrollByTime(focusedEntry.entryEndUtcMillis - toMillis + HALF_HOUR_MILLIS);
+ }
+ }
+ }
+
@Override
public View focusSearch(View focused, int direction) {
TableEntry focusedEntry = ((ProgramItemView) focused).getTableEntry();
long fromMillis = mProgramManager.getFromUtcMillis();
long toMillis = mProgramManager.getToUtcMillis();
- if (isDirectionStart(direction) || direction == View.FOCUS_BACKWARD) {
+ if (!mProgramGuide.isAccessibilityEnabled()
+ && (isDirectionStart(direction) || direction == View.FOCUS_BACKWARD)) {
if (focusedEntry.entryStartUtcMillis < fromMillis) {
// The current entry starts outside of the view; Align or scroll to the left.
scrollByTime(
@@ -162,7 +178,9 @@ public class ProgramRow extends TimelineGridView {
TableEntry targetEntry = ((ProgramItemView) target).getTableEntry();
if (isDirectionStart(direction) || direction == View.FOCUS_BACKWARD) {
- if (targetEntry.entryStartUtcMillis < fromMillis
+ if (mProgramGuide.isAccessibilityEnabled()) {
+ scrollByTime(targetEntry.entryStartUtcMillis - fromMillis);
+ } else if (targetEntry.entryStartUtcMillis < fromMillis
&& targetEntry.entryEndUtcMillis < fromMillis + HALF_HOUR_MILLIS) {
// The target entry starts outside the view; Align or scroll to the left.
scrollByTime(
diff --git a/src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java b/src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java
new file mode 100644
index 00000000..5e498be4
--- /dev/null
+++ b/src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java
@@ -0,0 +1,64 @@
+/*
+ * 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.guide;
+
+import android.os.Bundle;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerViewAccessibilityDelegate;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/** AccessibilityDelegate for {@link ProgramRow} */
+class ProgramRowAccessibilityDelegate extends RecyclerViewAccessibilityDelegate {
+ private final ItemDelegate mItemDelegate;
+
+ ProgramRowAccessibilityDelegate(RecyclerView recyclerView) {
+ super(recyclerView);
+
+ mItemDelegate =
+ new ItemDelegate(this) {
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ // Prevent Accessibility service to move the Program Row elements
+ // Ignoring Accessibility action above Set Text
+ // (accessibilityActionShowOnScreen)
+ if (action > AccessibilityNodeInfo.ACTION_SET_TEXT) {
+ return false;
+ }
+
+ return super.performAccessibilityAction(host, action, args);
+ }
+ };
+ }
+
+ @Override
+ public ItemDelegate getItemDelegate() {
+ return mItemDelegate;
+ }
+
+ @Override
+ public boolean onRequestSendAccessibilityEvent(
+ ViewGroup host, View child, AccessibilityEvent event) {
+ // Forcing the next item to be visible for scrolling in forward direction
+ if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
+ ((ProgramRow) host).focusSearchAccessibility(child, View.FOCUS_FORWARD);
+ }
+ return super.onRequestSendAccessibilityEvent(host, child, event);
+ }
+}
diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java
index 6e7485ac..7576bf50 100644
--- a/src/com/android/tv/guide/ProgramTableAdapter.java
+++ b/src/com/android/tv/guide/ProgramTableAdapter.java
@@ -110,6 +110,8 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr
private final int mDvrPaddingStartWithTrack;
private final int mDvrPaddingStartWithOutTrack;
+ private RecyclerView mRecyclerView;
+
ProgramTableAdapter(Context context, ProgramGuide programGuide) {
mContext = context;
mAccessibilityManager =
@@ -198,7 +200,15 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr
mProgramManager.addTableEntriesUpdatedListener(listAdapter);
mProgramListAdapters.add(listAdapter);
}
- notifyDataSetChanged();
+ if (mRecyclerView != null && mRecyclerView.isComputingLayout()) {
+ // it means that RecyclerView is in a lockdown state and any attempt to update adapter
+ // contents will result in an exception because adapter contents cannot be changed while
+ // RecyclerView is trying to compute the layout
+ // postpone the change using a Handler
+ mHandler.post(this::notifyDataSetChanged);
+ } else {
+ notifyDataSetChanged();
+ }
}
@Override
@@ -238,8 +248,22 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr
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);
- notifyItemChanged(channelIndex, true);
+ if (channelIndex >= 0 && channelIndex < mProgramListAdapters.size()) {
+ mProgramListAdapters.get(channelIndex).notifyItemChanged(pos, tableEntry);
+ notifyItemChanged(channelIndex, true);
+ }
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+ mRecyclerView = recyclerView;
+ super.onAttachedToRecyclerView(recyclerView);
+ }
+
+ @Override
+ public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
+ super.onDetachedFromRecyclerView(recyclerView);
+ mRecyclerView = null;
}
class ProgramRowViewHolder extends RecyclerView.ViewHolder
@@ -260,13 +284,7 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr
}
}
};
- private final Runnable mUpdateDetailViewRunnable =
- new Runnable() {
- @Override
- public void run() {
- updateDetailView();
- }
- };
+ private final Runnable mUpdateDetailViewRunnable = this::updateDetailView;
private final RecyclerView.OnScrollListener mOnScrollListener =
new RecyclerView.OnScrollListener() {
@@ -420,12 +438,14 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr
mChannelNumberView.setText(displayNumber);
mChannelNumberView.setVisibility(View.VISIBLE);
}
+
+ boolean isChannelLocked = isChannelLocked(channel);
mChannelNumberView.setTextColor(
- isChannelLocked(channel) ? mChannelBlockedTextColor : mChannelTextColor);
+ isChannelLocked ? mChannelBlockedTextColor : mChannelTextColor);
mChannelLogoView.setImageBitmap(null);
mChannelLogoView.setVisibility(View.GONE);
- if (isChannelLocked(channel)) {
+ if (isChannelLocked) {
mChannelNameView.setVisibility(View.GONE);
mChannelBlockView.setVisibility(View.VISIBLE);
} else {
@@ -573,13 +593,7 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr
mTitleView.setText(text);
}
- updateTextView(
- mTimeView,
- Utils.getDurationString(
- context,
- program.getStartTimeUtcMillis(),
- program.getEndTimeUtcMillis(),
- false));
+ updateTextView(mTimeView, program.getDurationString(context));
boolean trackMetaDataVisible =
updateTextView(
diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java
index 8536ef1f..4a9e4765 100644
--- a/src/com/android/tv/menu/ChannelsRowAdapter.java
+++ b/src/com/android/tv/menu/ChannelsRowAdapter.java
@@ -20,6 +20,9 @@ import android.content.Context;
import android.content.Intent;
import android.media.tv.TvInputInfo;
import android.view.View;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
+import com.android.tv.ChannelChanger;
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.analytics.Tracker;
@@ -34,9 +37,8 @@ import java.util.ArrayList;
import java.util.List;
/** An adapter of the Channels row. */
-public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<ChannelsRowItem> {
- // There are four special cards: guide, setup, dvr, applink.
- private static final int SIZE_OF_VIEW_TYPE = 5;
+public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<ChannelsRowItem>
+ implements AccessibilityStateChangeListener {
private final Context mContext;
private final Tracker mTracker;
@@ -44,58 +46,9 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels
private final DvrDataManager mDvrDataManager;
private final int mMaxCount;
private final int mMinCount;
+ private final ChannelChanger mChannelChanger;
- private final View.OnClickListener mGuideOnClickListener =
- new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- mTracker.sendMenuClicked(R.string.channels_item_program_guide);
- getMainActivity().getOverlayManager().showProgramGuide();
- }
- };
-
- private final View.OnClickListener mSetupOnClickListener =
- new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- mTracker.sendMenuClicked(R.string.channels_item_setup);
- getMainActivity().getOverlayManager().showSetupFragment();
- }
- };
-
- private final View.OnClickListener mDvrOnClickListener =
- new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- mTracker.sendMenuClicked(R.string.channels_item_dvr);
- getMainActivity().getOverlayManager().showDvrManager();
- }
- };
-
- private final View.OnClickListener mAppLinkOnClickListener =
- new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- mTracker.sendMenuClicked(R.string.channels_item_app_link);
- Intent intent = ((AppLinkCardView) view).getIntent();
- if (intent != null) {
- getMainActivity().startActivitySafe(intent);
- }
- }
- };
-
- private final View.OnClickListener mChannelOnClickListener =
- new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- // Always send the label "Channels" because the channel ID or name or number
- // might be
- // sensitive.
- mTracker.sendMenuClicked(R.string.menu_title_channels);
- getMainActivity().tuneToChannel((Channel) view.getTag());
- getMainActivity().hideOverlaysForTune();
- }
- };
+ private boolean mShowChannelUpDown;
public ChannelsRowAdapter(
Context context, Recommender recommender, int minCount, int maxCount) {
@@ -112,6 +65,11 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels
mMinCount = minCount;
mMaxCount = maxCount;
setHasStableIds(true);
+ mChannelChanger = (ChannelChanger) (context);
+ AccessibilityManager accessibilityManager =
+ context.getSystemService(AccessibilityManager.class);
+ mShowChannelUpDown = accessibilityManager.isEnabled();
+ accessibilityManager.addAccessibilityStateChangeListener(this);
}
@Override
@@ -133,18 +91,22 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels
public void onBindViewHolder(MyViewHolder viewHolder, int position) {
int viewType = getItemViewType(position);
if (viewType == R.layout.menu_card_guide) {
- viewHolder.itemView.setOnClickListener(mGuideOnClickListener);
+ viewHolder.itemView.setOnClickListener(this::onGuideClicked);
+ } else if (viewType == R.layout.menu_card_up) {
+ viewHolder.itemView.setOnClickListener(this::onChannelUpClicked);
+ } else if (viewType == R.layout.menu_card_down) {
+ viewHolder.itemView.setOnClickListener(this::onChannelDownClicked);
} else if (viewType == R.layout.menu_card_setup) {
- viewHolder.itemView.setOnClickListener(mSetupOnClickListener);
+ viewHolder.itemView.setOnClickListener(this::onSetupClicked);
} else if (viewType == R.layout.menu_card_app_link) {
- viewHolder.itemView.setOnClickListener(mAppLinkOnClickListener);
+ viewHolder.itemView.setOnClickListener(this::onAppLinkClicked);
} else if (viewType == R.layout.menu_card_dvr) {
- viewHolder.itemView.setOnClickListener(mDvrOnClickListener);
+ viewHolder.itemView.setOnClickListener(this::onDvrClicked);
SimpleCardView view = (SimpleCardView) viewHolder.itemView;
view.setText(R.string.channels_item_dvr);
} else {
viewHolder.itemView.setTag(getItemList().get(position).getChannel());
- viewHolder.itemView.setOnClickListener(mChannelOnClickListener);
+ viewHolder.itemView.setOnClickListener(this::onChannelClicked);
}
super.onBindViewHolder(viewHolder, position);
}
@@ -158,9 +120,53 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels
}
}
+ private void onGuideClicked(View unused) {
+ mTracker.sendMenuClicked(R.string.channels_item_program_guide);
+ getMainActivity().getOverlayManager().showProgramGuide();
+ }
+
+ private void onChannelDownClicked(View unused) {
+ mChannelChanger.channelDown();
+ }
+
+ private void onChannelUpClicked(View unused) {
+ mChannelChanger.channelUp();
+ }
+
+ private void onSetupClicked(View unused) {
+ mTracker.sendMenuClicked(R.string.channels_item_setup);
+ getMainActivity().getOverlayManager().showSetupFragment();
+ }
+
+ private void onDvrClicked(View unused) {
+ mTracker.sendMenuClicked(R.string.channels_item_dvr);
+ getMainActivity().getOverlayManager().showDvrManager();
+ }
+
+ private void onAppLinkClicked(View view) {
+ mTracker.sendMenuClicked(R.string.channels_item_app_link);
+ Intent intent = ((AppLinkCardView) view).getIntent();
+ if (intent != null) {
+ getMainActivity().startActivitySafe(intent);
+ }
+ }
+
+ private void onChannelClicked(View view) {
+ // Always send the label "Channels" because the channel ID or name or number might be
+ // sensitive.
+ mTracker.sendMenuClicked(R.string.menu_title_channels);
+ getMainActivity().tuneToChannel((Channel) view.getTag());
+ getMainActivity().hideOverlaysForTune();
+ }
+
private void createItems() {
List<ChannelsRowItem> items = new ArrayList<>();
items.add(ChannelsRowItem.GUIDE_ITEM);
+ if (mShowChannelUpDown) {
+ items.add(ChannelsRowItem.UP_ITEM);
+ items.add(ChannelsRowItem.DOWN_ITEM);
+ }
+
if (needToShowSetupItem()) {
items.add(ChannelsRowItem.SETUP_ITEM);
}
@@ -183,6 +189,12 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels
// The current index of the item list to iterate. It starts from 1 because the first item
// (GUIDE) is always visible and not updated.
int currentIndex = 1;
+ if (updateItem(mShowChannelUpDown, ChannelsRowItem.UP_ITEM, currentIndex)) {
+ ++currentIndex;
+ }
+ if (updateItem(mShowChannelUpDown, ChannelsRowItem.DOWN_ITEM, currentIndex)) {
+ ++currentIndex;
+ }
if (updateItem(needToShowSetupItem(), ChannelsRowItem.SETUP_ITEM, currentIndex)) {
++currentIndex;
}
@@ -298,4 +310,10 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels
channelList.add(channel);
return true;
}
+
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ mShowChannelUpDown = enabled;
+ update();
+ }
}
diff --git a/src/com/android/tv/menu/ChannelsRowItem.java b/src/com/android/tv/menu/ChannelsRowItem.java
index 608bb36e..12976ef2 100644
--- a/src/com/android/tv/menu/ChannelsRowItem.java
+++ b/src/com/android/tv/menu/ChannelsRowItem.java
@@ -30,6 +30,10 @@ public class ChannelsRowItem {
public static final int DVR_ITEM_ID = -3;
/** The item ID for app link item */
public static final int APP_LINK_ITEM_ID = -4;
+ /** The item ID for channel up item */
+ public static final int UP_ID = -5;
+ /** The item ID for app link item */
+ public static final int DOWN_ID = -6;
/** The item which represents the guide. */
public static final ChannelsRowItem GUIDE_ITEM =
@@ -44,6 +48,12 @@ public class ChannelsRowItem {
public static final ChannelsRowItem APP_LINK_ITEM =
new ChannelsRowItem(APP_LINK_ITEM_ID, R.layout.menu_card_app_link);
+ /** The item which represents the channel up. */
+ public static final ChannelsRowItem UP_ITEM = new ChannelsRowItem(UP_ID, R.layout.menu_card_up);
+ /** The item which represents the channel down. */
+ public static final ChannelsRowItem DOWN_ITEM =
+ new ChannelsRowItem(DOWN_ID, R.layout.menu_card_down);
+
private final long mItemId;
@NonNull private Channel mChannel;
private final int mLayoutId;
diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java
index 19a93dbc..6bdbf87b 100644
--- a/src/com/android/tv/menu/Menu.java
+++ b/src/com/android/tv/menu/Menu.java
@@ -213,12 +213,9 @@ public class Menu implements AccessibilityStateChangeListener {
rowIdToSelect,
mAnimationDisabledForTest
? null
- : new Runnable() {
- @Override
- public void run() {
- if (isActive()) {
- mShowAnimator.start();
- }
+ : () -> {
+ if (isActive()) {
+ mShowAnimator.start();
}
});
scheduleHide();
diff --git a/src/com/android/tv/menu/MenuAction.java b/src/com/android/tv/menu/MenuAction.java
index 52372535..8c180cae 100644
--- a/src/com/android/tv/menu/MenuAction.java
+++ b/src/com/android/tv/menu/MenuAction.java
@@ -50,12 +50,12 @@ public class MenuAction {
new MenuAction(
R.string.options_item_more_channels,
TvOptionsManager.OPTION_MORE_CHANNELS,
- R.drawable.ic_store);
+ R.drawable.ic_app_store);
public static final MenuAction DEV_ACTION =
new MenuAction(
R.string.options_item_developer,
TvOptionsManager.OPTION_DEVELOPER,
- R.drawable.ic_developer_mode_tv_white_48dp);
+ R.drawable.quantum_ic_developer_mode_tv_white_48);
public static final MenuAction SETTINGS_ACTION =
new MenuAction(
R.string.options_item_settings,
diff --git a/src/com/android/tv/menu/OptionsRowAdapter.java b/src/com/android/tv/menu/OptionsRowAdapter.java
index ceffe861..4e69a601 100644
--- a/src/com/android/tv/menu/OptionsRowAdapter.java
+++ b/src/com/android/tv/menu/OptionsRowAdapter.java
@@ -37,17 +37,14 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter<
public void onClick(View view) {
final MenuAction action = (MenuAction) view.getTag();
view.post(
- new Runnable() {
- @Override
- public void run() {
- int resId = action.getActionNameResId();
- if (resId == 0) {
- mTracker.sendMenuClicked(CUSTOM_ACTION_LABEL);
- } else {
- mTracker.sendMenuClicked(resId);
- }
- executeAction(action.getType());
+ () -> {
+ int resId = action.getActionNameResId();
+ if (resId == 0) {
+ mTracker.sendMenuClicked(CUSTOM_ACTION_LABEL);
+ } else {
+ mTracker.sendMenuClicked(resId);
}
+ executeAction(action.getType());
});
}
};
diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java
index 496d1969..0ce74ae1 100644
--- a/src/com/android/tv/menu/PlayControlsRowView.java
+++ b/src/com/android/tv/menu/PlayControlsRowView.java
@@ -185,13 +185,10 @@ public class PlayControlsRowView extends MenuRowView {
R.drawable.lb_ic_skip_previous,
R.string.play_controls_description_skip_previous,
null,
- new Runnable() {
- @Override
- public void run() {
- if (mTimeShiftManager.isAvailable()) {
- mTimeShiftManager.jumpToPrevious();
- updateControls(true);
- }
+ () -> {
+ if (mTimeShiftManager.isAvailable()) {
+ mTimeShiftManager.jumpToPrevious();
+ updateControls(true);
}
});
initializeButton(
@@ -199,13 +196,10 @@ public class PlayControlsRowView extends MenuRowView {
R.drawable.lb_ic_fast_rewind,
R.string.play_controls_description_fast_rewind,
null,
- new Runnable() {
- @Override
- public void run() {
- if (mTimeShiftManager.isAvailable()) {
- mTimeShiftManager.rewind();
- updateButtons();
- }
+ () -> {
+ if (mTimeShiftManager.isAvailable()) {
+ mTimeShiftManager.rewind();
+ updateButtons();
}
});
initializeButton(
@@ -213,13 +207,10 @@ public class PlayControlsRowView extends MenuRowView {
R.drawable.lb_ic_play,
R.string.play_controls_description_play_pause,
null,
- new Runnable() {
- @Override
- public void run() {
- if (mTimeShiftManager.isAvailable()) {
- mTimeShiftManager.togglePlayPause();
- updateButtons();
- }
+ () -> {
+ if (mTimeShiftManager.isAvailable()) {
+ mTimeShiftManager.togglePlayPause();
+ updateButtons();
}
});
initializeButton(
@@ -227,13 +218,10 @@ public class PlayControlsRowView extends MenuRowView {
R.drawable.lb_ic_fast_forward,
R.string.play_controls_description_fast_forward,
null,
- new Runnable() {
- @Override
- public void run() {
- if (mTimeShiftManager.isAvailable()) {
- mTimeShiftManager.fastForward();
- updateButtons();
- }
+ () -> {
+ if (mTimeShiftManager.isAvailable()) {
+ mTimeShiftManager.fastForward();
+ updateButtons();
}
});
initializeButton(
@@ -241,13 +229,10 @@ public class PlayControlsRowView extends MenuRowView {
R.drawable.lb_ic_skip_next,
R.string.play_controls_description_skip_next,
null,
- new Runnable() {
- @Override
- public void run() {
- if (mTimeShiftManager.isAvailable()) {
- mTimeShiftManager.jumpToNext();
- updateControls(true);
- }
+ () -> {
+ if (mTimeShiftManager.isAvailable()) {
+ mTimeShiftManager.jumpToNext();
+ updateControls(true);
}
});
int color =
@@ -257,12 +242,7 @@ public class PlayControlsRowView extends MenuRowView {
R.drawable.ic_record_start,
R.string.channels_item_record_start,
color,
- new Runnable() {
- @Override
- public void run() {
- onRecordButtonClicked();
- }
- });
+ this::onRecordButtonClicked);
}
private boolean isCurrentChannelRecording() {
@@ -296,13 +276,9 @@ public class PlayControlsRowView extends MenuRowView {
DvrUiHelper.checkStorageStatusAndShowErrorMessage(
mMainActivity,
currentChannel.getInputId(),
- new Runnable() {
- @Override
- public void run() {
+ () ->
DvrUiHelper.requestRecordingCurrentProgram(
- mMainActivity, currentChannel, program, true);
- }
- });
+ mMainActivity, currentChannel, program, true));
}
} else if (currentChannel != null) {
DvrUiHelper.showStopRecordingDialog(
@@ -490,15 +466,12 @@ public class PlayControlsRowView extends MenuRowView {
// After the focus is actually changed, hideRippleAnimation should run
// to reflect the result of the focus change. To be sure, hideRippleAnimation is posted.
post(
- new Runnable() {
- @Override
- public void run() {
- mJumpPreviousButton.hideRippleAnimation();
- mRewindButton.hideRippleAnimation();
- mPlayPauseButton.hideRippleAnimation();
- mFastForwardButton.hideRippleAnimation();
- mJumpNextButton.hideRippleAnimation();
- }
+ () -> {
+ mJumpPreviousButton.hideRippleAnimation();
+ mRewindButton.hideRippleAnimation();
+ mPlayPauseButton.hideRippleAnimation();
+ mFastForwardButton.hideRippleAnimation();
+ mJumpNextButton.hideRippleAnimation();
});
}
diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java
index 55affb59..fe52b25e 100644
--- a/src/com/android/tv/menu/TvOptionsRowAdapter.java
+++ b/src/com/android/tv/menu/TvOptionsRowAdapter.java
@@ -18,12 +18,11 @@ package com.android.tv.menu;
import android.content.Context;
import android.media.tv.TvTrackInfo;
-import android.support.annotation.VisibleForTesting;
-import com.android.tv.TvFeatures;
import com.android.tv.TvOptionsManager;
import com.android.tv.common.customization.CustomAction;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.data.DisplayMode;
+import com.android.tv.features.TvFeatures;
import com.android.tv.ui.TvViewUiManager;
import com.android.tv.ui.sidepanel.ClosedCaptionFragment;
import com.android.tv.ui.sidepanel.DeveloperOptionFragment;
@@ -78,7 +77,6 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
}
}
- @VisibleForTesting
private boolean updateClosedCaptionAction() {
return updateActionDescription(MenuAction.SELECT_CLOSED_CAPTION_ACTION);
}
diff --git a/src/com/android/tv/modules/TvApplicationModule.java b/src/com/android/tv/modules/TvApplicationModule.java
new file mode 100644
index 00000000..45383ae1
--- /dev/null
+++ b/src/com/android/tv/modules/TvApplicationModule.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018 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.modules;
+
+import android.content.Context;
+import com.android.tv.MainActivity;
+import com.android.tv.TvApplication;
+import com.android.tv.common.concurrent.NamedThreadFactory;
+import com.android.tv.common.dagger.ApplicationModule;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
+import com.android.tv.onboarding.OnboardingActivity;
+import com.android.tv.util.AsyncDbTask;
+import com.android.tv.util.TvInputManagerHelper;
+import dagger.Module;
+import dagger.Provides;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import javax.inject.Singleton;
+
+/** Dagger module for {@link TvApplication}. */
+@Module(
+ includes = {
+ ApplicationModule.class,
+ TvSingletonsModule.class,
+ MainActivity.Module.class,
+ OnboardingActivity.Module.class
+ })
+public class TvApplicationModule {
+ private static final NamedThreadFactory THREAD_FACTORY = new NamedThreadFactory("tv-app-db");
+
+ @Provides
+ @AsyncDbTask.DbExecutor
+ @Singleton
+ Executor providesDbExecutor() {
+ return Executors.newSingleThreadExecutor(THREAD_FACTORY);
+ }
+
+ @Provides
+ @Singleton
+ TvInputManagerHelper providesTvInputManagerHelper(@ApplicationContext Context context) {
+ TvInputManagerHelper tvInputManagerHelper = new TvInputManagerHelper(context);
+ tvInputManagerHelper.start();
+ return tvInputManagerHelper;
+ }
+}
diff --git a/src/com/android/tv/modules/TvSingletonsModule.java b/src/com/android/tv/modules/TvSingletonsModule.java
new file mode 100644
index 00000000..f998c08b
--- /dev/null
+++ b/src/com/android/tv/modules/TvSingletonsModule.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 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.modules;
+
+import com.android.tv.TvSingletons;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.ProgramDataManager;
+import dagger.Module;
+import dagger.Provides;
+
+/**
+ * Provides bindings for items provided by {@link TvSingletons}.
+ *
+ * <p>Use this module to inject items directly instead of using {@code TvSingletons}.
+ */
+@Module
+@SuppressWarnings("deprecation")
+public class TvSingletonsModule {
+ private final TvSingletons mTvSingletons;
+
+ public TvSingletonsModule(TvSingletons mTvSingletons) {
+ this.mTvSingletons = mTvSingletons;
+ }
+
+ @Provides
+ ChannelDataManager providesChannelDataManager() {
+ return mTvSingletons.getChannelDataManager();
+ }
+
+ @Provides
+ ProgramDataManager providesProgramDataManager() {
+ return mTvSingletons.getProgramDataManager();
+ }
+}
diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java
index a1cf9de1..776ae664 100644
--- a/src/com/android/tv/onboarding/OnboardingActivity.java
+++ b/src/com/android/tv/onboarding/OnboardingActivity.java
@@ -37,6 +37,9 @@ import com.android.tv.data.ChannelDataManager;
import com.android.tv.util.OnboardingUtils;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.TvInputManagerHelper;
+import dagger.android.AndroidInjection;
+import dagger.android.ContributesAndroidInjector;
+import javax.inject.Inject;
public class OnboardingActivity extends SetupActivity {
private static final String KEY_INTENT_AFTER_COMPLETION = "key_intent_after_completion";
@@ -47,9 +50,9 @@ public class OnboardingActivity extends SetupActivity {
private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1;
- private ChannelDataManager mChannelDataManager;
+ @Inject ChannelDataManager mChannelDataManager;
private TvInputManagerHelper mInputManager;
- private SetupUtils mSetupUtils;
+ @Inject SetupUtils mSetupUtils;
private final ChannelDataManager.Listener mChannelListener =
new ChannelDataManager.Listener() {
@Override
@@ -80,12 +83,11 @@ public class OnboardingActivity extends SetupActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
+ AndroidInjection.inject(this);
super.onCreate(savedInstanceState);
TvSingletons singletons = TvSingletons.getSingletons(this);
mInputManager = singletons.getTvInputManagerHelper();
- mSetupUtils = singletons.getSetupUtils();
if (PermissionUtils.hasAccessAllEpg(this) || PermissionUtils.hasReadTvListings(this)) {
- mChannelDataManager = singletons.getChannelDataManager();
// Make the channels of the new inputs which have been setup outside Live TV
// browsable.
if (mChannelDataManager.isDbLoadFinished()) {
@@ -148,13 +150,7 @@ public class OnboardingActivity extends SetupActivity {
private void showMerchantCollection() {
executeActionWithDelay(
- new Runnable() {
- @Override
- public void run() {
- startActivity(OnboardingUtils.ONLINE_STORE_INTENT);
- }
- },
- SHOW_RIPPLE_DURATION_MS);
+ () -> startActivity(OnboardingUtils.ONLINE_STORE_INTENT), SHOW_RIPPLE_DURATION_MS);
}
@Override
@@ -228,4 +224,11 @@ public class OnboardingActivity extends SetupActivity {
}
return false;
}
+
+ /** Exports {@link OnboardingActivity} for Dagger codegen to create the appropriate injector. */
+ @dagger.Module
+ public abstract static class Module {
+ @ContributesAndroidInjector
+ abstract OnboardingActivity contributeOnboardingActivityInjector();
+ }
}
diff --git a/src/com/android/tv/onboarding/SetupSourcesFragment.java b/src/com/android/tv/onboarding/SetupSourcesFragment.java
index f032f622..3566c9c3 100644
--- a/src/com/android/tv/onboarding/SetupSourcesFragment.java
+++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java
@@ -197,9 +197,13 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
mChannelDataManager.addListener(mChannelDataManagerListener);
super.onCreate(savedInstanceState);
mParentFragment = (SetupSourcesFragment) getParentFragment();
- singletons
- .getTunerInputController()
- .executeNetworkTunerDiscoveryAsyncTask(getContext());
+ if (singletons.getBuiltInTunerManager().isPresent()) {
+ singletons
+ .getBuiltInTunerManager()
+ .get()
+ .getTunerInputController()
+ .executeNetworkTunerDiscoveryAsyncTask(getContext());
+ }
}
@Override
@@ -332,7 +336,7 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
.id(ACTION_ONLINE_STORE)
.title(getString(R.string.setup_store_action_title))
.description(getString(R.string.setup_store_action_description))
- .icon(R.drawable.ic_store)
+ .icon(R.drawable.ic_app_store)
.build());
if (newPosition != -1) {
diff --git a/src/com/android/tv/parental/ContentRatingSystem.java b/src/com/android/tv/parental/ContentRatingSystem.java
index 600aaca1..d85dd50e 100644
--- a/src/com/android/tv/parental/ContentRatingSystem.java
+++ b/src/com/android/tv/parental/ContentRatingSystem.java
@@ -31,13 +31,10 @@ public class ContentRatingSystem {
* A comparator that implements the display order of a group of content rating systems.
*/
public static final Comparator<ContentRatingSystem> DISPLAY_NAME_COMPARATOR =
- new Comparator<ContentRatingSystem>() {
- @Override
- public int compare(ContentRatingSystem s1, ContentRatingSystem s2) {
- String name1 = s1.getDisplayName();
- String name2 = s2.getDisplayName();
- return name1.compareTo(name2);
- }
+ (ContentRatingSystem s1, ContentRatingSystem s2) -> {
+ String name1 = s1.getDisplayName();
+ String name2 = s2.getDisplayName();
+ return name1.compareTo(name2);
};
private static final String DELIMITER = "/";
diff --git a/src/com/android/tv/parental/ParentalControlSettings.java b/src/com/android/tv/parental/ParentalControlSettings.java
index db1f0a4d..b41b160e 100644
--- a/src/com/android/tv/parental/ParentalControlSettings.java
+++ b/src/com/android/tv/parental/ParentalControlSettings.java
@@ -24,6 +24,7 @@ import com.android.tv.parental.ContentRatingSystem.Rating;
import com.android.tv.parental.ContentRatingSystem.SubRating;
import com.android.tv.util.TvSettings;
import com.android.tv.util.TvSettings.ContentRatingLevel;
+import com.google.common.collect.ImmutableList;
import java.util.HashSet;
import java.util.Set;
@@ -160,6 +161,26 @@ public class ParentalControlSettings {
}
/**
+ * Checks whether any of given ratings is blocked and returns the first blocked rating.
+ *
+ * @param ratings The array of ratings to check
+ * @return The {@link TvContentRating} that is blocked.
+ */
+ public TvContentRating getBlockedRating(ImmutableList<TvContentRating> ratings) {
+ if (ratings == null || ratings.isEmpty()) {
+ return mTvInputManager.isRatingBlocked(TvContentRating.UNRATED)
+ ? TvContentRating.UNRATED
+ : null;
+ }
+ for (TvContentRating rating : ratings) {
+ if (mTvInputManager.isRatingBlocked(rating)) {
+ return rating;
+ }
+ }
+ return null;
+ }
+
+ /**
* Sets the blocked status of a given content rating.
*
* <p>Note that a call to this method automatically changes the current rating level to {@code
@@ -178,34 +199,14 @@ public class ParentalControlSettings {
/**
* Checks whether any of given ratings is blocked.
*
- * @param ratings The array of ratings to check
+ * @param ratings The list of ratings to check
* @return {@code true} if a rating is blocked, {@code false} otherwise.
*/
- public boolean isRatingBlocked(TvContentRating[] ratings) {
+ public boolean isRatingBlocked(ImmutableList<TvContentRating> ratings) {
return getBlockedRating(ratings) != null;
}
/**
- * Checks whether any of given ratings is blocked and returns the first blocked rating.
- *
- * @param ratings The array of ratings to check
- * @return The {@link TvContentRating} that is blocked.
- */
- public TvContentRating getBlockedRating(TvContentRating[] ratings) {
- if (ratings == null || ratings.length <= 0) {
- return mTvInputManager.isRatingBlocked(TvContentRating.UNRATED)
- ? TvContentRating.UNRATED
- : null;
- }
- for (TvContentRating rating : ratings) {
- if (mTvInputManager.isRatingBlocked(rating)) {
- return rating;
- }
- }
- return null;
- }
-
- /**
* Checks whether a given rating is blocked by the user or not.
*
* @param contentRatingSystem The content rating system where the given rating belongs.
diff --git a/src/com/android/tv/perf/EventNames.java b/src/com/android/tv/perf/EventNames.java
index 54745f3b..4d21d6d8 100644
--- a/src/com/android/tv/perf/EventNames.java
+++ b/src/com/android/tv/perf/EventNames.java
@@ -25,31 +25,39 @@ import java.lang.annotation.Retention;
* Constants for performance event names.
*
* <p>Only constants are used to insure no PII is sent.
- *
+
*/
public final class EventNames {
@Retention(SOURCE)
@StringDef({
- APPLICATION_ONCREATE,
FETCH_EPG_TASK,
- MAIN_ACTIVITY_ONCREATE,
- MAIN_ACTIVITY_ONSTART,
- MAIN_ACTIVITY_ONRESUME,
- ON_DEVICE_SEARCH
+ ON_DEVICE_SEARCH,
+ PROGRAM_GUIDE_SHOW,
+ PROGRAM_DATA_MANAGER_PROGRAMS_PREFETCH_TASK_DO_IN_BACKGROUND,
+ PROGRAM_GUIDE_SHOW_FROM_EMPTY_CACHE,
+ PROGRAM_GUIDE_SCROLL_HORIZONTALLY,
+ PROGRAM_GUIDE_SCROLL_VERTICALLY,
+ MEMORY_ON_PROGRAM_GUIDE_CLOSE
})
public @interface EventName {}
- public static final String APPLICATION_ONCREATE = "Application.onCreate";
public static final String FETCH_EPG_TASK = "FetchEpgTask";
- public static final String MAIN_ACTIVITY_ONCREATE = "MainActivity.onCreate";
- public static final String MAIN_ACTIVITY_ONSTART = "MainActivity.onStart";
- public static final String MAIN_ACTIVITY_ONRESUME = "MainActivity.onResume";
/**
* Event name for query running time of on-device search in {@link
* com.android.tv.search.LocalSearchProvider}.
*/
public static final String ON_DEVICE_SEARCH = "OnDeviceSearch";
+ public static final String PROGRAM_GUIDE_SHOW = "ProgramGuide.show";
+ public static final String PROGRAM_DATA_MANAGER_PROGRAMS_PREFETCH_TASK_DO_IN_BACKGROUND =
+ "ProgramDataManager.ProgramsPrefetchTask.doInBackground";
+ public static final String PROGRAM_GUIDE_SHOW_FROM_EMPTY_CACHE =
+ "ProgramGuide.show.fromEmptyCache";
+ public static final String PROGRAM_GUIDE_SCROLL_HORIZONTALLY =
+ "ProgramGuide.scroll.horizontally";
+ public static final String PROGRAM_GUIDE_SCROLL_VERTICALLY = "ProgramGuide.scroll.vertically";
+ public static final String MEMORY_ON_PROGRAM_GUIDE_CLOSE = "ProgramGuide.memory.close";
+
private EventNames() {}
}
diff --git a/src/com/android/tv/perf/PerformanceMonitor.java b/src/com/android/tv/perf/PerformanceMonitor.java
index 111aa851..b1ae759d 100644
--- a/src/com/android/tv/perf/PerformanceMonitor.java
+++ b/src/com/android/tv/perf/PerformanceMonitor.java
@@ -19,6 +19,7 @@ package com.android.tv.perf;
import static com.android.tv.perf.EventNames.EventName;
import android.content.Context;
+import com.google.errorprone.annotations.CompileTimeConstant;
/** Measures Performance. */
public interface PerformanceMonitor {
@@ -34,7 +35,7 @@ public interface PerformanceMonitor {
*
* @param eventName to record
*/
- void recordMemory(@EventName String eventName);
+ void recordMemory(@EventName @CompileTimeConstant String eventName);
/**
* Starts a timer for a global event to allow measuring the event's latency across activities If
@@ -42,7 +43,7 @@ public interface PerformanceMonitor {
*
* @param eventName for which the timer starts
*/
- void startGlobalTimer(@EventName String eventName);
+ void startGlobalTimer(@EventName @CompileTimeConstant String eventName);
/**
* Stops a cross activities timer for a specific eventName and records the timer duration. If no
@@ -50,7 +51,7 @@ public interface PerformanceMonitor {
*
* @param eventName for which the timer stops
*/
- void stopGlobalTimer(@EventName String eventName);
+ void stopGlobalTimer(@EventName @CompileTimeConstant String eventName);
/**
* Starts a timer to record latency of a specific scenario or event. Use this method to track
@@ -69,7 +70,7 @@ public interface PerformanceMonitor {
* @param event that needs to be stopped
* @param eventName for which the timer stops. This must be constant with no PII.
*/
- void stopTimer(TimerEvent event, @EventName String eventName);
+ void stopTimer(TimerEvent event, @EventName @CompileTimeConstant String eventName);
/**
* Starts recording jank for a specific scenario or event.
@@ -79,14 +80,14 @@ public interface PerformanceMonitor {
*
* @param eventName of the event for which tracking is started
*/
- void startJankRecorder(@EventName String eventName);
+ void startJankRecorder(@EventName @CompileTimeConstant String eventName);
/**
* Stops recording jank for a specific event and records the jank event.
*
* @param eventName of the event that needs to be stopped
*/
- void stopJankRecorder(@EventName String eventName);
+ void stopJankRecorder(@EventName @CompileTimeConstant String eventName);
/**
* Starts activity to display PerformanceMonitor events recorded in local database for debug
diff --git a/src/com/android/tv/perf/PerformanceMonitorManager.java b/src/com/android/tv/perf/PerformanceMonitorManager.java
new file mode 100644
index 00000000..db6667d1
--- /dev/null
+++ b/src/com/android/tv/perf/PerformanceMonitorManager.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2018 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.perf;
+
+import android.app.Application;
+
+/** Manages the initialization of Performance Monitoring. */
+public interface PerformanceMonitorManager {
+
+ /**
+ * Initializes the {@link com.android.tv.perf.PerformanceMonitor}.
+ *
+ * <p>This should only be called once.
+ */
+ PerformanceMonitor initialize(Application app);
+
+ /**
+ * Returns a lightweight object to help measure both cold and warm startup latency.
+ *
+ * <p>This method is idempotent and lightweight. It can be called multiple times and does not
+ * need to be cached.
+ */
+ StartupMeasure getStartupMeasure();
+}
diff --git a/src/com/android/tv/perf/PerformanceMonitorManagerFactory.java b/src/com/android/tv/perf/PerformanceMonitorManagerFactory.java
new file mode 100644
index 00000000..fe3ea14b
--- /dev/null
+++ b/src/com/android/tv/perf/PerformanceMonitorManagerFactory.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 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.perf;
+
+import com.android.tv.perf.stub.StubPerformanceMonitorManager;
+import javax.inject.Inject;
+
+public final class PerformanceMonitorManagerFactory {
+ private static final PerformanceMonitorManagerFactory INSTANCE =
+ new PerformanceMonitorManagerFactory();
+
+ @Inject
+ public PerformanceMonitorManagerFactory() {}
+
+ public static PerformanceMonitorManager create() {
+ return INSTANCE.get();
+ }
+
+ public PerformanceMonitorManager get() {
+ return new StubPerformanceMonitorManager();
+ }
+}
diff --git a/src/com/android/tv/perf/StartupMeasure.java b/src/com/android/tv/perf/StartupMeasure.java
new file mode 100644
index 00000000..5cf183ca
--- /dev/null
+++ b/src/com/android/tv/perf/StartupMeasure.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 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.perf;
+
+import android.app.Activity;
+import android.app.Application;
+
+/**
+ * Measures App startup. This interface is lightweight to help measure both cold and warm startup
+ * latency. Implementations must not throw any Exception.
+ */
+public interface StartupMeasure {
+
+ /** To be be placed as the first static block in the app's Application class. */
+ void onAppClassLoaded();
+
+ /**
+ * To be placed in your {@link Application#onCreate} to let Performance Monitoring know when
+ * this happen.
+ */
+ void onAppCreate(Application application);
+
+ /**
+ * To be placed in an initialization block of your {@link Activity} to let Performance
+ * Monitoring know when this activity is instantiated. Please note that this initialization
+ * block should be before other initialization blocks (if any) in your activity class.
+ */
+ void onActivityInit();
+}
diff --git a/src/com/android/tv/perf/StubPerformanceMonitor.java b/src/com/android/tv/perf/stub/StubPerformanceMonitor.java
index 3742a2a7..80c2f6c5 100644
--- a/src/com/android/tv/perf/StubPerformanceMonitor.java
+++ b/src/com/android/tv/perf/stub/StubPerformanceMonitor.java
@@ -14,20 +14,17 @@
* limitations under the License.
*/
-package com.android.tv.perf;
+package com.android.tv.perf.stub;
-import android.app.Application;
import android.content.Context;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.TimerEvent;
/** Do nothing implementation of {@link PerformanceMonitor}. */
public final class StubPerformanceMonitor implements PerformanceMonitor {
private static final TimerEvent TIMER_EVENT = new TimerEvent() {};
- public static PerformanceMonitor initialize(Application app) {
- return new StubPerformanceMonitor();
- }
-
@Override
public void startMemoryMonitor() {}
diff --git a/src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java b/src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java
new file mode 100644
index 00000000..0c268155
--- /dev/null
+++ b/src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 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.perf.stub;
+
+import android.app.Application;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.PerformanceMonitorManager;
+import com.android.tv.perf.StartupMeasure;
+
+/** Manages a stub implementation of Performance Monitoring. */
+public class StubPerformanceMonitorManager implements PerformanceMonitorManager {
+
+ @Override
+ public PerformanceMonitor initialize(Application app) {
+ return new StubPerformanceMonitor();
+ }
+
+ @Override
+ public StartupMeasure getStartupMeasure() {
+ return new StubStartupMeasure();
+ }
+}
diff --git a/src/com/android/tv/perf/stub/StubStartupMeasure.java b/src/com/android/tv/perf/stub/StubStartupMeasure.java
new file mode 100644
index 00000000..d4412261
--- /dev/null
+++ b/src/com/android/tv/perf/stub/StubStartupMeasure.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2018 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.perf.stub;
+
+import android.app.Application;
+import com.android.tv.perf.StartupMeasure;
+
+/** Stub implementation of {@link StartupMeasure} */
+public class StubStartupMeasure implements StartupMeasure {
+
+ @Override
+ public void onAppClassLoaded() {}
+
+ @Override
+ public void onAppCreate(Application application) {}
+
+ @Override
+ public void onActivityInit() {}
+}
diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java
index d8528bb5..0eb03bec 100644
--- a/src/com/android/tv/receiver/BootCompletedReceiver.java
+++ b/src/com/android/tv/receiver/BootCompletedReceiver.java
@@ -25,10 +25,10 @@ import android.os.Build;
import android.util.Log;
import com.android.tv.Starter;
import com.android.tv.TvActivity;
-import com.android.tv.TvFeatures;
import com.android.tv.TvSingletons;
import com.android.tv.dvr.recorder.DvrRecordingService;
import com.android.tv.dvr.recorder.RecordingScheduler;
+import com.android.tv.features.TvFeatures;
import com.android.tv.recommendation.ChannelPreviewUpdater;
import com.android.tv.recommendation.NotificationService;
import com.android.tv.util.OnboardingUtils;
@@ -70,7 +70,7 @@ public class BootCompletedReceiver extends BroadcastReceiver {
// Grant permission to already set up packages after the system has finished booting.
SetupUtils.grantEpgPermissionToSetUpPackages(context);
- if (TvFeatures.UNHIDE.isEnabled(context)) {
+ if (TvFeatures.UNHIDE.isEnabled(context.getApplicationContext())) {
if (OnboardingUtils.isFirstBoot(context)) {
// Enable the application if this is the first "unhide" feature is enabled just in
// case when the app has been disabled before.
diff --git a/src/com/android/tv/receiver/PackageIntentsReceiver.java b/src/com/android/tv/receiver/PackageIntentsReceiver.java
index 07f5d6be..5bc6d724 100644
--- a/src/com/android/tv/receiver/PackageIntentsReceiver.java
+++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java
@@ -22,8 +22,8 @@ import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import com.android.tv.Starter;
-import com.android.tv.TvFeatures;
import com.android.tv.TvSingletons;
+import com.android.tv.features.TvFeatures;
import com.android.tv.util.Partner;
import com.google.android.tv.partner.support.EpgContract;
diff --git a/src/com/android/tv/recommendation/ChannelPreviewUpdater.java b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
index 410b8252..2590a337 100644
--- a/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
+++ b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
@@ -25,9 +25,9 @@ import android.content.Context;
import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.RequiresApi;
-import android.support.media.tv.TvContractCompat;
import android.text.TextUtils;
import android.util.Log;
+import androidx.tvprovider.media.tv.TvContractCompat;
import com.android.tv.Starter;
import com.android.tv.TvSingletons;
import com.android.tv.data.PreviewDataManager;
@@ -169,18 +169,23 @@ public class ChannelPreviewUpdater {
@Override
protected Set<Program> doInBackground(Void... params) {
Set<Program> programs = new HashSet<>();
- List<Channel> channels = new ArrayList<>(mRecommender.recommendChannels());
- for (Channel channel : channels) {
- if (channel.isPhysicalTunerChannel()) {
- final Program program = Utils.getCurrentProgram(mContext, channel.getId());
- if (program != null
- && isChannelRecommendationApplicable(channel, program)) {
- programs.add(program);
- if (programs.size() >= RECOMMENDATION_COUNT) {
- break;
+ try {
+ List<Channel> channels = new ArrayList<>(mRecommender.recommendChannels());
+ for (Channel channel : channels) {
+ if (channel.isPhysicalTunerChannel()) {
+ final Program program =
+ Utils.getCurrentProgram(mContext, channel.getId());
+ if (program != null
+ && isChannelRecommendationApplicable(channel, program)) {
+ programs.add(program);
+ if (programs.size() >= RECOMMENDATION_COUNT) {
+ break;
+ }
}
}
}
+ } catch (Exception e) {
+ Log.w(TAG, "Can't update preview data", e);
}
return programs;
}
@@ -241,6 +246,17 @@ public class ChannelPreviewUpdater {
}
}
});
+ } else if (mJobService != null && mJobParams != null) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Preview channel not created because there is only "
+ + programs.size()
+ + " programs");
+ }
+ mJobService.jobFinished(mJobParams, false);
+ mJobService = null;
+ mJobParams = null;
}
} else {
updatePreviewProgramsForPreviewChannel(
diff --git a/src/com/android/tv/recommendation/RecommendationDataManager.java b/src/com/android/tv/recommendation/RecommendationDataManager.java
index 649920fb..fc20031c 100644
--- a/src/com/android/tv/recommendation/RecommendationDataManager.java
+++ b/src/com/android/tv/recommendation/RecommendationDataManager.java
@@ -33,6 +33,7 @@ import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
+import android.util.Log;
import com.android.tv.TvSingletons;
import com.android.tv.common.WeakHandler;
import com.android.tv.common.util.PermissionUtils;
@@ -52,6 +53,7 @@ import java.util.concurrent.ConcurrentHashMap;
/** Manages teh data need to make recommendations. */
public class RecommendationDataManager implements WatchedHistoryManager.Listener {
+ private static final String TAG = "RecommendationDataManag";
private static final int MSG_START = 1000;
private static final int MSG_STOP = 1001;
private static final int MSG_UPDATE_CHANNELS = 1002;
@@ -187,13 +189,7 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
mMainHandler = new RecommendationMainHandler(Looper.getMainLooper(), this);
mContentObserver = new RecommendationContentObserver(mHandler);
mChannelDataManager = TvSingletons.getSingletons(mContext).getChannelDataManager();
- runOnMainThread(
- new Runnable() {
- @Override
- public void run() {
- start();
- }
- });
+ runOnMainThread(this::start);
}
/**
@@ -202,13 +198,10 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
*/
public void release(@NonNull final Listener listener) {
runOnMainThread(
- new Runnable() {
- @Override
- public void run() {
- removeListener(listener);
- if (mListeners.size() == 0) {
- stop();
- }
+ () -> {
+ removeListener(listener);
+ if (mListeners.size() == 0) {
+ stop();
}
});
}
@@ -257,13 +250,7 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
}
private void addListener(Listener listener) {
- runOnMainThread(
- new Runnable() {
- @Override
- public void run() {
- mListeners.add(listener);
- }
- });
+ runOnMainThread(() -> mListeners.add(listener));
}
@MainThread
@@ -347,18 +334,18 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
history.add(createWatchedProgramFromWatchedProgramCursor(cursor));
} while (cursor.moveToPrevious());
}
+ } catch (Exception e) {
+ Log.e(TAG, "Error trying to load watch history from " + uri, e);
+ return;
}
for (WatchedProgram watchedProgram : history) {
final ChannelRecord channelRecord =
updateChannelRecordFromWatchedProgram(watchedProgram);
if (mChannelRecordMapLoaded && channelRecord != null) {
runOnMainThread(
- new Runnable() {
- @Override
- public void run() {
- for (Listener l : mListeners) {
- l.onNewWatchLog(channelRecord);
- }
+ () -> {
+ for (Listener l : mListeners) {
+ l.onNewWatchLog(channelRecord);
}
});
}
@@ -397,12 +384,9 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
convertFromWatchedHistoryManagerRecords(watchedRecord));
if (mChannelRecordMapLoaded && channelRecord != null) {
runOnMainThread(
- new Runnable() {
- @Override
- public void run() {
- for (Listener l : mListeners) {
- l.onNewWatchLog(channelRecord);
- }
+ () -> {
+ for (Listener l : mListeners) {
+ l.onNewWatchLog(channelRecord);
}
});
}
@@ -441,24 +425,18 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
private void onNotifyChannelRecordMapLoaded() {
mChannelRecordMapLoaded = true;
runOnMainThread(
- new Runnable() {
- @Override
- public void run() {
- for (Listener l : mListeners) {
- l.onChannelRecordLoaded();
- }
+ () -> {
+ for (Listener l : mListeners) {
+ l.onChannelRecordLoaded();
}
});
}
private void onNotifyChannelRecordMapChanged() {
runOnMainThread(
- new Runnable() {
- @Override
- public void run() {
- for (Listener l : mListeners) {
- l.onChannelRecordChanged();
- }
+ () -> {
+ for (Listener l : mListeners) {
+ l.onChannelRecordChanged();
}
});
}
diff --git a/src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java b/src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java
deleted file mode 100644
index 528096dd..00000000
--- a/src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java
+++ /dev/null
@@ -1,363 +0,0 @@
-/*
- * Copyright (C) 2018 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.search;
-
-import android.support.annotation.Nullable;
-
-/**
- * Hand copy of generated Autovalue class.
- *
- * TODO get autovalue working
- */
-
-final class AutoValue_LocalSearchProvider_SearchResult extends LocalSearchProvider.SearchResult {
-
- private final long channelId;
- private final String channelNumber;
- private final String title;
- private final String description;
- private final String imageUri;
- private final String intentAction;
- private final String intentData;
- private final String intentExtraData;
- private final String contentType;
- private final boolean isLive;
- private final int videoWidth;
- private final int videoHeight;
- private final long duration;
- private final int progressPercentage;
-
- private AutoValue_LocalSearchProvider_SearchResult(
- long channelId,
- @Nullable String channelNumber,
- @Nullable String title,
- @Nullable String description,
- @Nullable String imageUri,
- @Nullable String intentAction,
- @Nullable String intentData,
- @Nullable String intentExtraData,
- @Nullable String contentType,
- boolean isLive,
- int videoWidth,
- int videoHeight,
- long duration,
- int progressPercentage) {
- this.channelId = channelId;
- this.channelNumber = channelNumber;
- this.title = title;
- this.description = description;
- this.imageUri = imageUri;
- this.intentAction = intentAction;
- this.intentData = intentData;
- this.intentExtraData = intentExtraData;
- this.contentType = contentType;
- this.isLive = isLive;
- this.videoWidth = videoWidth;
- this.videoHeight = videoHeight;
- this.duration = duration;
- this.progressPercentage = progressPercentage;
- }
-
- @Override
- long getChannelId() {
- return channelId;
- }
-
- @Nullable
- @Override
- String getChannelNumber() {
- return channelNumber;
- }
-
- @Nullable
- @Override
- String getTitle() {
- return title;
- }
-
- @Nullable
- @Override
- String getDescription() {
- return description;
- }
-
- @Nullable
- @Override
- String getImageUri() {
- return imageUri;
- }
-
- @Nullable
- @Override
- String getIntentAction() {
- return intentAction;
- }
-
- @Nullable
- @Override
- String getIntentData() {
- return intentData;
- }
-
- @Nullable
- @Override
- String getIntentExtraData() {
- return intentExtraData;
- }
-
- @Nullable
- @Override
- String getContentType() {
- return contentType;
- }
-
- @Override
- boolean getIsLive() {
- return isLive;
- }
-
- @Override
- int getVideoWidth() {
- return videoWidth;
- }
-
- @Override
- int getVideoHeight() {
- return videoHeight;
- }
-
- @Override
- long getDuration() {
- return duration;
- }
-
- @Override
- int getProgressPercentage() {
- return progressPercentage;
- }
-
- @Override
- public String toString() {
- return "SearchResult{"
- + "channelId=" + channelId + ", "
- + "channelNumber=" + channelNumber + ", "
- + "title=" + title + ", "
- + "description=" + description + ", "
- + "imageUri=" + imageUri + ", "
- + "intentAction=" + intentAction + ", "
- + "intentData=" + intentData + ", "
- + "intentExtraData=" + intentExtraData + ", "
- + "contentType=" + contentType + ", "
- + "isLive=" + isLive + ", "
- + "videoWidth=" + videoWidth + ", "
- + "videoHeight=" + videoHeight + ", "
- + "duration=" + duration + ", "
- + "progressPercentage=" + progressPercentage
- + "}";
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof LocalSearchProvider.SearchResult) {
- LocalSearchProvider.SearchResult that = (LocalSearchProvider.SearchResult) o;
- return (this.channelId == that.getChannelId())
- && ((this.channelNumber == null) ? (that.getChannelNumber() == null) : this.channelNumber.equals(that.getChannelNumber()))
- && ((this.title == null) ? (that.getTitle() == null) : this.title.equals(that.getTitle()))
- && ((this.description == null) ? (that.getDescription() == null) : this.description.equals(that.getDescription()))
- && ((this.imageUri == null) ? (that.getImageUri() == null) : this.imageUri.equals(that.getImageUri()))
- && ((this.intentAction == null) ? (that.getIntentAction() == null) : this.intentAction.equals(that.getIntentAction()))
- && ((this.intentData == null) ? (that.getIntentData() == null) : this.intentData.equals(that.getIntentData()))
- && ((this.intentExtraData == null) ? (that.getIntentExtraData() == null) : this.intentExtraData.equals(that.getIntentExtraData()))
- && ((this.contentType == null) ? (that.getContentType() == null) : this.contentType.equals(that.getContentType()))
- && (this.isLive == that.getIsLive())
- && (this.videoWidth == that.getVideoWidth())
- && (this.videoHeight == that.getVideoHeight())
- && (this.duration == that.getDuration())
- && (this.progressPercentage == that.getProgressPercentage());
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int h$ = 1;
- h$ *= 1000003;
- h$ ^= (int) ((channelId >>> 32) ^ channelId);
- h$ *= 1000003;
- h$ ^= (channelNumber == null) ? 0 : channelNumber.hashCode();
- h$ *= 1000003;
- h$ ^= (title == null) ? 0 : title.hashCode();
- h$ *= 1000003;
- h$ ^= (description == null) ? 0 : description.hashCode();
- h$ *= 1000003;
- h$ ^= (imageUri == null) ? 0 : imageUri.hashCode();
- h$ *= 1000003;
- h$ ^= (intentAction == null) ? 0 : intentAction.hashCode();
- h$ *= 1000003;
- h$ ^= (intentData == null) ? 0 : intentData.hashCode();
- h$ *= 1000003;
- h$ ^= (intentExtraData == null) ? 0 : intentExtraData.hashCode();
- h$ *= 1000003;
- h$ ^= (contentType == null) ? 0 : contentType.hashCode();
- h$ *= 1000003;
- h$ ^= isLive ? 1231 : 1237;
- h$ *= 1000003;
- h$ ^= videoWidth;
- h$ *= 1000003;
- h$ ^= videoHeight;
- h$ *= 1000003;
- h$ ^= (int) ((duration >>> 32) ^ duration);
- h$ *= 1000003;
- h$ ^= progressPercentage;
- return h$;
- }
-
- static final class Builder extends LocalSearchProvider.SearchResult.Builder {
- private Long channelId;
- private String channelNumber;
- private String title;
- private String description;
- private String imageUri;
- private String intentAction;
- private String intentData;
- private String intentExtraData;
- private String contentType;
- private Boolean isLive;
- private Integer videoWidth;
- private Integer videoHeight;
- private Long duration;
- private Integer progressPercentage;
- Builder() {
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setChannelId(long channelId) {
- this.channelId = channelId;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setChannelNumber(@Nullable String channelNumber) {
- this.channelNumber = channelNumber;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setTitle(@Nullable String title) {
- this.title = title;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setDescription(@Nullable String description) {
- this.description = description;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setImageUri(@Nullable String imageUri) {
- this.imageUri = imageUri;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setIntentAction(@Nullable String intentAction) {
- this.intentAction = intentAction;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setIntentData(@Nullable String intentData) {
- this.intentData = intentData;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setIntentExtraData(@Nullable String intentExtraData) {
- this.intentExtraData = intentExtraData;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setContentType(@Nullable String contentType) {
- this.contentType = contentType;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setIsLive(boolean isLive) {
- this.isLive = isLive;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setVideoWidth(int videoWidth) {
- this.videoWidth = videoWidth;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setVideoHeight(int videoHeight) {
- this.videoHeight = videoHeight;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setDuration(long duration) {
- this.duration = duration;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult.Builder setProgressPercentage(int progressPercentage) {
- this.progressPercentage = progressPercentage;
- return this;
- }
- @Override
- LocalSearchProvider.SearchResult build() {
- String missing = "";
- if (this.channelId == null) {
- missing += " channelId";
- }
- if (this.isLive == null) {
- missing += " isLive";
- }
- if (this.videoWidth == null) {
- missing += " videoWidth";
- }
- if (this.videoHeight == null) {
- missing += " videoHeight";
- }
- if (this.duration == null) {
- missing += " duration";
- }
- if (this.progressPercentage == null) {
- missing += " progressPercentage";
- }
- if (!missing.isEmpty()) {
- throw new IllegalStateException("Missing required properties:" + missing);
- }
- return new AutoValue_LocalSearchProvider_SearchResult(
- this.channelId,
- this.channelNumber,
- this.title,
- this.description,
- this.imageUri,
- this.intentAction,
- this.intentData,
- this.intentExtraData,
- this.contentType,
- this.isLive,
- this.videoWidth,
- this.videoHeight,
- this.duration,
- this.progressPercentage);
- }
- }
-
-}
diff --git a/src/com/android/tv/search/DataManagerSearch.java b/src/com/android/tv/search/DataManagerSearch.java
index 82fb5016..a649c0ac 100644
--- a/src/com/android/tv/search/DataManagerSearch.java
+++ b/src/com/android/tv/search/DataManagerSearch.java
@@ -34,12 +34,12 @@ import com.android.tv.data.api.Channel;
import com.android.tv.search.LocalSearchProvider.SearchResult;
import com.android.tv.util.MainThreadExecutor;
import com.android.tv.util.Utils;
+import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@@ -68,13 +68,7 @@ public class DataManagerSearch implements SearchInterface {
public List<SearchResult> search(final String query, final int limit, final int action) {
Future<List<SearchResult>> future =
MainThreadExecutor.getInstance()
- .submit(
- new Callable<List<SearchResult>>() {
- @Override
- public List<SearchResult> call() throws Exception {
- return searchFromDataManagers(query, limit, action);
- }
- });
+ .submit(() -> searchFromDataManagers(query, limit, action));
try {
return future.get();
@@ -255,7 +249,7 @@ public class DataManagerSearch implements SearchInterface {
result.setIntentData(buildIntentData(channelId));
result.setContentType(Programs.CONTENT_ITEM_TYPE);
result.setIsLive(true);
- result.setProgressPercentage(LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE);
+ result.setProgressPercentage(SearchInterface.PROGRESS_PERCENTAGE_HIDE);
} else {
result.setTitle(program.getTitle());
result.setDescription(
@@ -299,7 +293,7 @@ public class DataManagerSearch implements SearchInterface {
private int getProgressPercentage(long startUtcMillis, long endUtcMillis) {
long current = System.currentTimeMillis();
if (startUtcMillis > current || endUtcMillis <= current) {
- return LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE;
+ return SearchInterface.PROGRESS_PERCENTAGE_HIDE;
}
return (int) (100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis));
}
@@ -308,10 +302,8 @@ public class DataManagerSearch implements SearchInterface {
return TvContract.buildChannelUri(channelId).toString();
}
- private boolean isRatingBlocked(TvContentRating[] ratings) {
- if (ratings == null
- || ratings.length == 0
- || !mTvInputManager.isParentalControlsEnabled()) {
+ private boolean isRatingBlocked(ImmutableList<TvContentRating> ratings) {
+ if (ratings == null || ratings.isEmpty() || !mTvInputManager.isParentalControlsEnabled()) {
return false;
}
for (TvContentRating rating : ratings) {
diff --git a/src/com/android/tv/search/LocalSearchProvider.java b/src/com/android/tv/search/LocalSearchProvider.java
index 97e7f229..5652c986 100644
--- a/src/com/android/tv/search/LocalSearchProvider.java
+++ b/src/com/android/tv/search/LocalSearchProvider.java
@@ -37,6 +37,7 @@ import com.android.tv.perf.EventNames;
import com.android.tv.perf.PerformanceMonitor;
import com.android.tv.perf.TimerEvent;
import com.android.tv.util.TvUriMatcher;
+import com.google.auto.value.AutoValue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -48,8 +49,6 @@ public class LocalSearchProvider extends ContentProvider {
/** The authority for LocalSearchProvider. */
public static final String AUTHORITY = CommonConstants.BASE_PACKAGE + ".search";
- public static final int PROGRESS_PERCENTAGE_HIDE = -1;
-
// TODO: Remove this once added to the SearchManager.
private static final String SUGGEST_COLUMN_PROGRESS_BAR_PERCENTAGE = "progress_bar_percentage";
@@ -223,7 +222,7 @@ public class LocalSearchProvider extends ContentProvider {
}
/** A placeholder to a search result. */
- // TODO(b/72052568): Get autovalue to work in aosp master
+ @AutoValue
public abstract static class SearchResult {
public static Builder builder() {
// primitive fields cannot be nullable. Set to default;
@@ -236,7 +235,7 @@ public class LocalSearchProvider extends ContentProvider {
.setProgressPercentage(0);
}
- // TODO(b/72052568): Get autovalue to work in aosp master
+ @AutoValue.Builder
abstract static class Builder {
abstract Builder setChannelId(long value);
diff --git a/src/com/android/tv/search/ProgramGuideSearchFragment.java b/src/com/android/tv/search/ProgramGuideSearchFragment.java
index cb26252b..6c94bd33 100644
--- a/src/com/android/tv/search/ProgramGuideSearchFragment.java
+++ b/src/com/android/tv/search/ProgramGuideSearchFragment.java
@@ -84,7 +84,7 @@ public class ProgramGuideSearchFragment extends SearchFragment {
createImageLoaderCallback(cardView));
} else {
cardView.setMainImage(
- mMainActivity.getDrawable(R.drawable.ic_live_channels_96x96));
+ mMainActivity.getDrawable(R.drawable.ic_tv_app_96x96));
}
}
@@ -171,7 +171,7 @@ public class ProgramGuideSearchFragment extends SearchFragment {
View v = super.onCreateView(inflater, container, savedInstanceState);
v.setBackgroundResource(R.color.program_guide_scrim);
- setBadgeDrawable(mMainActivity.getDrawable(R.drawable.ic_live_channels_96x96));
+ setBadgeDrawable(mMainActivity.getDrawable(R.drawable.ic_tv_app_96x96));
setSearchResultProvider(mSearchResultProvider);
setOnItemViewClickedListener(mItemClickedListener);
return v;
diff --git a/src/com/android/tv/search/SearchInterface.java b/src/com/android/tv/search/SearchInterface.java
index 4866ee84..d16270ed 100644
--- a/src/com/android/tv/search/SearchInterface.java
+++ b/src/com/android/tv/search/SearchInterface.java
@@ -26,6 +26,7 @@ public interface SearchInterface {
int ACTION_TYPE_SWITCH_CHANNEL = 2;
int ACTION_TYPE_SWITCH_INPUT = 3;
int ACTION_TYPE_END = 3;
+ int PROGRESS_PERCENTAGE_HIDE = -1;
/**
* Search channels, inputs, or programs. This assumes that parental control settings will not be
diff --git a/src/com/android/tv/search/TvProviderSearch.java b/src/com/android/tv/search/TvProviderSearch.java
index 92197f2d..8a1f51f9 100644
--- a/src/com/android/tv/search/TvProviderSearch.java
+++ b/src/com/android/tv/search/TvProviderSearch.java
@@ -36,6 +36,7 @@ import com.android.tv.common.TvContentRatingCache;
import com.android.tv.common.util.PermissionUtils;
import com.android.tv.search.LocalSearchProvider.SearchResult;
import com.android.tv.util.Utils;
+import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -270,7 +271,7 @@ public class TvProviderSearch implements SearchInterface {
result.setIntentData(buildIntentData(id));
result.setContentType(Programs.CONTENT_ITEM_TYPE);
result.setIsLive(true);
- result.setProgressPercentage(LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE);
+ result.setProgressPercentage(SearchInterface.PROGRESS_PERCENTAGE_HIDE);
searchResults.add(result.build());
@@ -343,7 +344,7 @@ public class TvProviderSearch implements SearchInterface {
private int getProgressPercentage(long startUtcMillis, long endUtcMillis) {
long current = System.currentTimeMillis();
if (startUtcMillis > current || endUtcMillis <= current) {
- return LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE;
+ return SearchInterface.PROGRESS_PERCENTAGE_HIDE;
}
return (int) (100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis));
}
@@ -481,7 +482,7 @@ public class TvProviderSearch implements SearchInterface {
if (TextUtils.isEmpty(ratings) || !mTvInputManager.isParentalControlsEnabled()) {
return false;
}
- TvContentRating[] ratingArray = mTvContentRatingCache.getRatings(ratings);
+ ImmutableList<TvContentRating> ratingArray = mTvContentRatingCache.getRatings(ratings);
if (ratingArray != null) {
for (TvContentRating r : ratingArray) {
if (mTvInputManager.isRatingBlocked(r)) {
diff --git a/src/com/android/tv/setup/SystemSetupActivity.java b/src/com/android/tv/setup/SystemSetupActivity.java
index c6b10e52..b2160b3a 100644
--- a/src/com/android/tv/setup/SystemSetupActivity.java
+++ b/src/com/android/tv/setup/SystemSetupActivity.java
@@ -64,13 +64,7 @@ public class SystemSetupActivity extends SetupActivity {
private void showMerchantCollection() {
executeActionWithDelay(
- new Runnable() {
- @Override
- public void run() {
- startActivity(OnboardingUtils.ONLINE_STORE_INTENT);
- }
- },
- SHOW_RIPPLE_DURATION_MS);
+ () -> startActivity(OnboardingUtils.ONLINE_STORE_INTENT), SHOW_RIPPLE_DURATION_MS);
}
@Override
diff --git a/src/com/android/tv/tuner/TunerInputController.java b/src/com/android/tv/tuner/TunerInputController.java
deleted file mode 100644
index 02611bbf..00000000
--- a/src/com/android/tv/tuner/TunerInputController.java
+++ /dev/null
@@ -1,556 +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.tuner;
-
-import android.app.AlarmManager;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.hardware.usb.UsbDevice;
-import android.hardware.usb.UsbManager;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.SystemClock;
-import android.preference.PreferenceManager;
-import android.support.annotation.NonNull;
-import android.text.TextUtils;
-import android.util.Log;
-import android.widget.Toast;
-import com.android.tv.R;
-import com.android.tv.Starter;
-import com.android.tv.TvApplication;
-import com.android.tv.TvSingletons;
-import com.android.tv.common.BuildConfig;
-import com.android.tv.common.util.SystemPropertiesProxy;
-
-
-import com.android.tv.tuner.setup.BaseTunerSetupActivity;
-import com.android.tv.tuner.util.TunerInputInfoUtils;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Controls the package visibility of {@link BaseTunerTvInputService}.
- *
- * <p>Listens to broadcast intent for {@link Intent#ACTION_BOOT_COMPLETED}, {@code
- * UsbManager.ACTION_USB_DEVICE_ATTACHED}, and {@code UsbManager.ACTION_USB_DEVICE_ATTACHED} to
- * update the connection status of the supported USB TV tuners.
- */
-public class TunerInputController {
- private static final boolean DEBUG = false;
- private static final String TAG = "TunerInputController";
- private static final String PREFERENCE_IS_NETWORK_TUNER_ATTACHED = "network_tuner";
- private static final String SECURITY_PATCH_LEVEL_KEY = "ro.build.version.security_patch";
- private static final String SECURITY_PATCH_LEVEL_FORMAT = "yyyy-MM-dd";
- private static final String PLAY_STORE_LINK_TEMPLATE = "market://details?id=%s";
-
- /** Action of {@link Intent} to check network connection repeatedly when it is necessary. */
- private static final String CHECKING_NETWORK_TUNER_STATUS =
- "com.android.tv.action.CHECKING_NETWORK_TUNER_STATUS";
-
- private static final String EXTRA_CHECKING_DURATION =
- "com.android.tv.action.extra.CHECKING_DURATION";
- private static final String EXTRA_DEVICE_IP = "com.android.tv.action.extra.DEVICE_IP";
-
- private static final long INITIAL_CHECKING_DURATION_MS = TimeUnit.SECONDS.toMillis(10);
- private static final long MAXIMUM_CHECKING_DURATION_MS = TimeUnit.MINUTES.toMillis(10);
- private static final String NOTIFICATION_CHANNEL_ID = "tuner_discovery_notification";
-
- // TODO: Load settings from XML file
- private static final TunerDevice[] TUNER_DEVICES = {
- new TunerDevice(0x2040, 0xb123, null), // WinTV-HVR-955Q
- new TunerDevice(0x07ca, 0x0837, null), // AverTV Volar Hybrid Q
- // WinTV-dualHD (bulk) will be supported after 2017 April security patch.
- new TunerDevice(0x2040, 0x826d, "2017-04-01"), // WinTV-dualHD (bulk)
- new TunerDevice(0x2040, 0x0264, null),
- };
-
- private static final int MSG_ENABLE_INPUT_SERVICE = 1000;
- private static final long DVB_DRIVER_CHECK_DELAY_MS = 300;
-
- private final ComponentName usbTunerComponent;
- private final ComponentName networkTunerComponent;
- private final ComponentName builtInTunerComponent;
- private final Map<TunerDevice, ComponentName> mTunerServiceMapping = new HashMap<>();
-
- private final Map<ComponentName, String> mTunerApplicationNames = new HashMap<>();
- private final Map<ComponentName, String> mNotificationMessages = new HashMap<>();
- private final Map<ComponentName, Bitmap> mNotificationLargeIcons = new HashMap<>();
-
- private final CheckDvbDeviceHandler mHandler = new CheckDvbDeviceHandler(this);
-
- public TunerInputController(ComponentName embeddedTuner) {
- usbTunerComponent = embeddedTuner;
- networkTunerComponent = usbTunerComponent;
- builtInTunerComponent = usbTunerComponent;
- for (TunerDevice device : TUNER_DEVICES) {
- mTunerServiceMapping.put(device, usbTunerComponent);
- }
- }
-
- /** Checks status of USB devices to see if there are available USB tuners connected. */
- public void onCheckingUsbTunerStatus(Context context, String action) {
- onCheckingUsbTunerStatus(context, action, mHandler);
- }
-
- private void onCheckingUsbTunerStatus(
- Context context, String action, @NonNull CheckDvbDeviceHandler handler) {
- Set<TunerDevice> connectedUsbTuners = getConnectedUsbTuners(context);
- handler.removeMessages(MSG_ENABLE_INPUT_SERVICE);
- if (!connectedUsbTuners.isEmpty()) {
- // Need to check if DVB driver is accessible. Since the driver creation
- // could be happen after the USB event, delay the checking by
- // DVB_DRIVER_CHECK_DELAY_MS.
- handler.sendMessageDelayed(
- handler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context),
- DVB_DRIVER_CHECK_DELAY_MS);
- } else {
- handleTunerStatusChanged(
- context,
- false,
- connectedUsbTuners,
- TextUtils.equals(action, UsbManager.ACTION_USB_DEVICE_DETACHED)
- ? TunerHal.TUNER_TYPE_USB
- : null);
- }
- }
-
- private void onNetworkTunerChanged(Context context, boolean enabled) {
- SharedPreferences sharedPreferences =
- PreferenceManager.getDefaultSharedPreferences(context);
- if (sharedPreferences.contains(PREFERENCE_IS_NETWORK_TUNER_ATTACHED)
- && sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)
- == enabled) {
- // the status is not changed
- return;
- }
- if (enabled) {
- sharedPreferences.edit().putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, true).apply();
- } else {
- sharedPreferences
- .edit()
- .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)
- .apply();
- }
- // Network tuner detection is initiated by UI. So the app should not
- // be killed.
- handleTunerStatusChanged(
- context, true, getConnectedUsbTuners(context), TunerHal.TUNER_TYPE_NETWORK);
- }
-
- /**
- * See if any USB tuner hardware is attached in the system.
- *
- * @param context {@link Context} instance
- * @return {@code true} if any tuner device we support is plugged in
- */
- private Set<TunerDevice> getConnectedUsbTuners(Context context) {
- UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
- Map<String, UsbDevice> deviceList = manager.getDeviceList();
- String currentSecurityLevel =
- SystemPropertiesProxy.getString(SECURITY_PATCH_LEVEL_KEY, null);
-
- Set<TunerDevice> devices = new HashSet<>();
- for (UsbDevice device : deviceList.values()) {
- if (DEBUG) {
- Log.d(TAG, "Device: " + device);
- }
- for (TunerDevice tuner : TUNER_DEVICES) {
- if (tuner.equalsTo(device) && tuner.isSupported(currentSecurityLevel)) {
- Log.i(TAG, "Tuner found");
- devices.add(tuner);
- }
- }
- }
- return devices;
- }
-
- private void handleTunerStatusChanged(
- Context context,
- boolean forceDontKillApp,
- Set<TunerDevice> connectedUsbTuners,
- Integer triggerType) {
- Map<ComponentName, Integer> serviceToEnable = new HashMap<>();
- Set<ComponentName> serviceToDisable = new HashSet<>();
- serviceToDisable.add(builtInTunerComponent);
- serviceToDisable.add(networkTunerComponent);
- if (TunerFeatures.TUNER.isEnabled(context)) {
- // TODO: support both built-in tuner and other tuners at the same time?
- if (TunerHal.useBuiltInTuner(context)) {
- enableTunerTvInputService(
- context, true, false, TunerHal.TUNER_TYPE_BUILT_IN, builtInTunerComponent);
- return;
- }
- SharedPreferences sharedPreferences =
- PreferenceManager.getDefaultSharedPreferences(context);
- if (sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)) {
- serviceToEnable.put(networkTunerComponent, TunerHal.TUNER_TYPE_NETWORK);
- }
- }
- for (TunerDevice device : TUNER_DEVICES) {
- if (TunerFeatures.TUNER.isEnabled(context) && connectedUsbTuners.contains(device)) {
- serviceToEnable.put(mTunerServiceMapping.get(device), TunerHal.TUNER_TYPE_USB);
- } else {
- serviceToDisable.add(mTunerServiceMapping.get(device));
- }
- }
- serviceToDisable.removeAll(serviceToEnable.keySet());
- for (ComponentName serviceComponent : serviceToEnable.keySet()) {
- if (isTunerPackageInstalled(context, serviceComponent)) {
- enableTunerTvInputService(
- context,
- true,
- forceDontKillApp,
- serviceToEnable.get(serviceComponent),
- serviceComponent);
- } else {
- sendNotificationToInstallPackage(context, serviceComponent);
- }
- }
- for (ComponentName serviceComponent : serviceToDisable) {
- if (isTunerPackageInstalled(context, serviceComponent)) {
- enableTunerTvInputService(
- context, false, forceDontKillApp, triggerType, serviceComponent);
- } else {
- cancelNotificationToInstallPackage(context, serviceComponent);
- }
- }
- }
-
- /**
- * Enable/disable the component {@link BaseTunerTvInputService}.
- *
- * @param context {@link Context} instance
- * @param enabled {@code true} to enable the service; otherwise {@code false}
- */
- private static void enableTunerTvInputService(
- Context context,
- boolean enabled,
- boolean forceDontKillApp,
- Integer tunerType,
- ComponentName serviceComponent) {
- if (DEBUG) Log.d(TAG, "enableTunerTvInputService: " + enabled);
- PackageManager pm = context.getPackageManager();
- int newState =
- enabled
- ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
- : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
- if (newState != pm.getComponentEnabledSetting(serviceComponent)) {
- int flags = forceDontKillApp ? PackageManager.DONT_KILL_APP : 0;
- if (serviceComponent.getPackageName().equals(context.getPackageName())) {
- // Don't kill APP when handling input count changing. Or the following
- // setComponentEnabledSetting() call won't work.
- ((TvApplication) context.getApplicationContext())
- .handleInputCountChanged(true, enabled, true);
- // Bundled input. Don't kill app if LiveChannels app is active since we don't want
- // to kill the running app.
- if (TvSingletons.getSingletons(context).getMainActivityWrapper().isCreated()) {
- flags |= PackageManager.DONT_KILL_APP;
- }
- // Send/cancel the USB tuner TV input setup notification.
- BaseTunerSetupActivity.onTvInputEnabled(context, enabled, tunerType);
- if (!enabled && tunerType != null) {
- if (tunerType == TunerHal.TUNER_TYPE_USB) {
- Toast.makeText(
- context,
- R.string.msg_usb_tuner_disconnected,
- Toast.LENGTH_SHORT)
- .show();
- } else if (tunerType == TunerHal.TUNER_TYPE_NETWORK) {
- Toast.makeText(
- context,
- R.string.msg_network_tuner_disconnected,
- Toast.LENGTH_SHORT)
- .show();
- }
- }
- }
- // Enable/disable the USB tuner TV input.
- pm.setComponentEnabledSetting(serviceComponent, newState, flags);
- if (DEBUG) Log.d(TAG, "Status updated:" + enabled);
- } else if (enabled && serviceComponent.getPackageName().equals(context.getPackageName())) {
- // When # of tuners is changed or the tuner input service is switching from/to using
- // network tuners or the device just boots.
- TunerInputInfoUtils.updateTunerInputInfo(context);
- }
- }
-
- /**
- * Discovers a network tuner. If the network connection is down, it won't repeatedly checking.
- */
- public void executeNetworkTunerDiscoveryAsyncTask(final Context context) {
- executeNetworkTunerDiscoveryAsyncTask(context, 0, 0);
- }
-
- /**
- * Discovers a network tuner.
- *
- * @param context {@link Context}
- * @param repeatedDurationMs The time length to wait to repeatedly check network status to start
- * finding network tuner when the network connection is not available. {@code 0} to disable
- * repeatedly checking.
- * @param deviceIp The previous discovered device IP, 0 if none.
- */
- private void executeNetworkTunerDiscoveryAsyncTask(
- final Context context, final long repeatedDurationMs, final int deviceIp) {
- if (!TunerFeatures.NETWORK_TUNER.isEnabled(context)) {
- return;
- }
- final Intent networkCheckingIntent = new Intent(context, IntentReceiver.class);
- networkCheckingIntent.setAction(CHECKING_NETWORK_TUNER_STATUS);
- if (!isNetworkConnected(context) && repeatedDurationMs > 0) {
- sendCheckingAlarm(context, networkCheckingIntent, repeatedDurationMs);
- } else {
- new AsyncTask<Void, Void, Boolean>() {
- @Override
- protected Boolean doInBackground(Void... params) {
- Boolean result = null;
- // Implement and execute network tuner discovery AsyncTask here.
- return result;
- }
-
- @Override
- protected void onPostExecute(Boolean foundNetworkTuner) {
- if (foundNetworkTuner == null) {
- return;
- }
- sendCheckingAlarm(
- context,
- networkCheckingIntent,
- foundNetworkTuner ? INITIAL_CHECKING_DURATION_MS : repeatedDurationMs);
- onNetworkTunerChanged(context, foundNetworkTuner);
- }
- }.execute();
- }
- }
-
- private static boolean isNetworkConnected(Context context) {
- ConnectivityManager cm =
- (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo networkInfo = cm.getActiveNetworkInfo();
- return networkInfo != null && networkInfo.isConnected();
- }
-
- private static void sendCheckingAlarm(Context context, Intent intent, long delayMs) {
- AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- intent.putExtra(EXTRA_CHECKING_DURATION, delayMs);
- PendingIntent alarmIntent =
- PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- alarmManager.set(
- AlarmManager.ELAPSED_REALTIME,
- SystemClock.elapsedRealtime() + delayMs,
- alarmIntent);
- }
-
- private static boolean isTunerPackageInstalled(
- Context context, ComponentName serviceComponent) {
- try {
- context.getPackageManager().getPackageInfo(serviceComponent.getPackageName(), 0);
- return true;
- } catch (NameNotFoundException e) {
- return false;
- }
- }
-
- private void sendNotificationToInstallPackage(Context context, ComponentName serviceComponent) {
- if (!BuildConfig.ENG) {
- return;
- }
- String applicationName = mTunerApplicationNames.get(serviceComponent);
- if (applicationName == null) {
- applicationName = context.getString(R.string.tuner_install_default_application_name);
- }
- String contentTitle =
- context.getString(
- R.string.tuner_install_notification_content_title, applicationName);
- String contentText = mNotificationMessages.get(serviceComponent);
- if (contentText == null) {
- contentText = context.getString(R.string.tuner_install_notification_content_text);
- }
- Bitmap largeIcon = mNotificationLargeIcons.get(serviceComponent);
- if (largeIcon == null) {
- // TODO: Make a better default image.
- largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_store);
- }
- NotificationManager notificationManager =
- (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- if (notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) == null) {
- createNotificationChannel(context, notificationManager);
- }
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setData(
- Uri.parse(
- String.format(
- PLAY_STORE_LINK_TEMPLATE, serviceComponent.getPackageName())));
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID);
- builder.setAutoCancel(true)
- .setSmallIcon(R.drawable.ic_launcher_s)
- .setLargeIcon(largeIcon)
- .setContentTitle(contentTitle)
- .setContentText(contentText)
- .setCategory(Notification.CATEGORY_RECOMMENDATION)
- .setContentIntent(PendingIntent.getActivity(context, 0, intent, 0));
- notificationManager.notify(serviceComponent.getPackageName(), 0, builder.build());
- }
-
- private static void cancelNotificationToInstallPackage(
- Context context, ComponentName serviceComponent) {
- NotificationManager notificationManager =
- (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.cancel(serviceComponent.getPackageName(), 0);
- }
-
- private static void createNotificationChannel(
- Context context, NotificationManager notificationManager) {
- notificationManager.createNotificationChannel(
- new NotificationChannel(
- NOTIFICATION_CHANNEL_ID,
- context.getResources()
- .getString(R.string.ut_setup_notification_channel_name),
- NotificationManager.IMPORTANCE_HIGH));
- }
-
- public static class IntentReceiver extends BroadcastReceiver {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent);
- Starter.start(context);
- TunerInputController tunerInputController =
- TvSingletons.getSingletons(context).getTunerInputController();
- if (!TunerFeatures.TUNER.isEnabled(context)) {
- tunerInputController.handleTunerStatusChanged(
- context, false, Collections.emptySet(), null);
- return;
- }
- switch (intent.getAction()) {
- case Intent.ACTION_BOOT_COMPLETED:
- tunerInputController.executeNetworkTunerDiscoveryAsyncTask(
- context, INITIAL_CHECKING_DURATION_MS, 0);
- // fall through
- case TvApplication.ACTION_APPLICATION_FIRST_LAUNCHED:
- case UsbManager.ACTION_USB_DEVICE_ATTACHED:
- case UsbManager.ACTION_USB_DEVICE_DETACHED:
- tunerInputController.onCheckingUsbTunerStatus(context, intent.getAction());
- break;
- case CHECKING_NETWORK_TUNER_STATUS:
- long repeatedDurationMs =
- intent.getLongExtra(
- EXTRA_CHECKING_DURATION, INITIAL_CHECKING_DURATION_MS);
- tunerInputController.executeNetworkTunerDiscoveryAsyncTask(
- context,
- Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS),
- intent.getIntExtra(EXTRA_DEVICE_IP, 0));
- break;
- default: // fall out
- }
- }
- }
-
- /**
- * Simple data holder for a USB device. Used to represent a tuner model, and compare against
- * {@link UsbDevice}.
- */
- private static class TunerDevice {
- private final int vendorId;
- private final int productId;
-
- // security patch level from which the specific tuner type is supported.
- private final String minSecurityLevel;
-
- private TunerDevice(int vendorId, int productId, String minSecurityLevel) {
- this.vendorId = vendorId;
- this.productId = productId;
- this.minSecurityLevel = minSecurityLevel;
- }
-
- private boolean equalsTo(UsbDevice device) {
- return device.getVendorId() == vendorId && device.getProductId() == productId;
- }
-
- private boolean isSupported(String currentSecurityLevel) {
- if (minSecurityLevel == null) {
- return true;
- }
-
- long supportSecurityLevelTimeStamp = 0;
- long currentSecurityLevelTimestamp = 0;
- try {
- SimpleDateFormat format = new SimpleDateFormat(SECURITY_PATCH_LEVEL_FORMAT);
- supportSecurityLevelTimeStamp = format.parse(minSecurityLevel).getTime();
- currentSecurityLevelTimestamp = format.parse(currentSecurityLevel).getTime();
- } catch (ParseException e) {
- }
- return supportSecurityLevelTimeStamp != 0
- && supportSecurityLevelTimeStamp <= currentSecurityLevelTimestamp;
- }
- }
-
- private static class CheckDvbDeviceHandler extends Handler {
-
- private final TunerInputController mTunerInputController;
- private DvbDeviceAccessor mDvbDeviceAccessor;
-
- CheckDvbDeviceHandler(TunerInputController tunerInputController) {
- super(Looper.getMainLooper());
- this.mTunerInputController = tunerInputController;
- }
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_ENABLE_INPUT_SERVICE:
- Context context = (Context) msg.obj;
- if (mDvbDeviceAccessor == null) {
- mDvbDeviceAccessor = new DvbDeviceAccessor(context);
- }
- boolean enabled = mDvbDeviceAccessor.isDvbDeviceAvailable();
- mTunerInputController.handleTunerStatusChanged(
- context,
- false,
- enabled
- ? mTunerInputController.getConnectedUsbTuners(context)
- : Collections.emptySet(),
- TunerHal.TUNER_TYPE_USB);
- break;
- default: // fall out
- }
- }
- }
-}
diff --git a/src/com/android/tv/tunerinputcontroller/BuiltInTunerManager.java b/src/com/android/tv/tunerinputcontroller/BuiltInTunerManager.java
new file mode 100644
index 00000000..e4fa35d9
--- /dev/null
+++ b/src/com/android/tv/tunerinputcontroller/BuiltInTunerManager.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2018 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.tunerinputcontroller;
+
+import com.android.tv.common.singletons.HasTvInputId;
+
+/** Controllers and parameters needed to access a built in tuner. */
+public interface BuiltInTunerManager extends HasTvInputId {
+ TunerInputController getTunerInputController();
+}
diff --git a/src/com/android/tv/tunerinputcontroller/HasBuiltInTunerManager.java b/src/com/android/tv/tunerinputcontroller/HasBuiltInTunerManager.java
new file mode 100644
index 00000000..90540bc3
--- /dev/null
+++ b/src/com/android/tv/tunerinputcontroller/HasBuiltInTunerManager.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2018 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.tunerinputcontroller;
+
+import com.google.common.base.Optional;
+
+/**
+ * Has optional {@link BuiltInTunerManager}.
+ *
+ * <p>If the {@code BuiltInTunerManager} is absent the built tuner is not enabled.
+ */
+public interface HasBuiltInTunerManager {
+
+ /** @deprecated inject instead */
+ @Deprecated
+ Optional<BuiltInTunerManager> getBuiltInTunerManager();
+}
diff --git a/src/com/android/tv/tunerinputcontroller/TunerInputController.java b/src/com/android/tv/tunerinputcontroller/TunerInputController.java
new file mode 100644
index 00000000..f822dbe0
--- /dev/null
+++ b/src/com/android/tv/tunerinputcontroller/TunerInputController.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 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.tunerinputcontroller;
+
+import android.content.Context;
+import android.content.Intent;
+
+/** Controls the package visibility of built in tuner services. */
+public interface TunerInputController {
+
+ Intent createSetupIntent(Context context);
+
+ void onCheckingUsbTunerStatus(Context context, String action);
+
+ void executeNetworkTunerDiscoveryAsyncTask(Context context);
+
+ /**
+ * Updates tuner input's info.
+ *
+ * @param context {@link Context} instance
+ */
+ void updateTunerInputInfo(Context context);
+}
diff --git a/src/com/android/tv/ui/AppLayerTvView.java b/src/com/android/tv/ui/AppLayerTvView.java
index b2be9f02..e2b64a1e 100644
--- a/src/com/android/tv/ui/AppLayerTvView.java
+++ b/src/com/android/tv/ui/AppLayerTvView.java
@@ -17,10 +17,10 @@
package com.android.tv.ui;
import android.content.Context;
-import android.media.tv.TvView;
import android.util.AttributeSet;
import android.view.SurfaceView;
import android.view.View;
+import com.android.tv.common.compat.TvViewCompat;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.common.util.Debug;
@@ -31,7 +31,7 @@ import com.android.tv.common.util.Debug;
* android.media.tv.TvView#setMain()} does not work because its implementation assumes that the app
* uses only application layer. TODO: remove this class once the TvView.setMain() is revisited.
*/
-public class AppLayerTvView extends TvView {
+public class AppLayerTvView extends TvViewCompat {
public AppLayerTvView(Context context) {
super(context);
}
diff --git a/src/com/android/tv/ui/BlockScreenView.java b/src/com/android/tv/ui/BlockScreenView.java
index 6b2d9a01..b7a2dd95 100644
--- a/src/com/android/tv/ui/BlockScreenView.java
+++ b/src/com/android/tv/ui/BlockScreenView.java
@@ -22,6 +22,7 @@ import android.animation.AnimatorInflater;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
@@ -180,6 +181,10 @@ public class BlockScreenView extends FrameLayout {
requestLayout();
}
+ public void setInfoTextOnClickListener(@Nullable OnClickListener onClickListener) {
+ mBlockingInfoTextView.setOnClickListener(onClickListener);
+ }
+
/** Changes the view layout according to the {@code blockScreenType}. */
public void onBlockStatusChanged(@BlockScreenType int blockScreenType, boolean withAnimation) {
if (!withAnimation) {
@@ -252,4 +257,8 @@ public class BlockScreenView extends FrameLayout {
mInfoFadeOut.end();
}
}
+
+ public void setInfoTextClickable(boolean clickable) {
+ mBlockingInfoTextView.setClickable(clickable);
+ }
}
diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java
index 28325197..00ac7e32 100644
--- a/src/com/android/tv/ui/ChannelBannerView.java
+++ b/src/com/android/tv/ui/ChannelBannerView.java
@@ -46,11 +46,10 @@ import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
-import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.singletons.HasSingletons;
import com.android.tv.data.Program;
import com.android.tv.data.StreamInfo;
import com.android.tv.data.api.Channel;
@@ -59,11 +58,14 @@ import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.ui.TvTransitionManager.TransitionLayout;
import com.android.tv.ui.hideable.AutoHideScheduler;
+import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import com.android.tv.util.images.ImageCache;
import com.android.tv.util.images.ImageLoader;
import com.android.tv.util.images.ImageLoader.ImageLoaderCallback;
import com.android.tv.util.images.ImageLoader.LoadTvInputLogoTask;
+import com.google.common.collect.ImmutableList;
+import javax.inject.Provider;
/** A view to render channel banner. */
public class ChannelBannerView extends FrameLayout
@@ -74,6 +76,21 @@ public class ChannelBannerView extends FrameLayout
/** Show all information at the channel banner. */
public static final int LOCK_NONE = 0;
+ /** Singletons needed for this class. */
+ public interface MySingletons {
+ Provider<Channel> getCurrentChannelProvider();
+
+ Provider<Program> getCurrentProgramProvider();
+
+ Provider<TvOverlayManager> getOverlayManagerProvider();
+
+ TvInputManagerHelper getTvInputManagerHelperSingleton();
+
+ Provider<Long> getCurrentPlayingPositionProvider();
+
+ DvrManager getDvrManagerSingleton();
+ }
+
/**
* Lock program details at the channel banner. This is used when a content is locked so we don't
* want to show program details including program description text and poster art.
@@ -94,14 +111,21 @@ public class ChannelBannerView extends FrameLayout
private Program mLockedChannelProgram;
private static String sClosedCaptionMark;
- private final MainActivity mMainActivity;
private final Resources mResources;
+ private final Provider<Channel> mCurrentChannelProvider;
+ private final Provider<Program> mCurrentProgramProvider;
+ private final Provider<Long> mCurrentPlayingPositionProvider;
+ private final TvInputManagerHelper mTvInputManagerHelper;
+ // TvOverlayManager is always created after ChannelBannerView
+ private final Provider<TvOverlayManager> mTvOverlayManager;
+
private View mChannelView;
private TextView mChannelNumberTextView;
private ImageView mChannelLogoImageView;
private TextView mProgramTextView;
private ImageView mTvInputLogoImageView;
+ private ImageView mChannelSignalStrengthView;
private TextView mChannelNameTextView;
private TextView mProgramTimeTextView;
private ProgressBar mRemainingTimeView;
@@ -143,6 +167,32 @@ public class ChannelBannerView extends FrameLayout
private final int mRecordingIconPadding;
private final Interpolator mResizeInterpolator;
+ /**
+ * 0 - 100 represent signal strength percentage. Strength is divided into 5 levels (0 - 4).
+ *
+ * <p>This is the upper boundary of level 0 [0%, 20%], and the lower boundary of level 1 (20%,
+ * 40%].
+ */
+ private static final int SIGNAL_STRENGTH_0_OF_4_UPPER_BOUND = 20;
+
+ /**
+ * This is the upper boundary of level 1 (20%, 40%], and the lower boundary of level 2 (40%,
+ * 60%].
+ */
+ private static final int SIGNAL_STRENGTH_1_OF_4_UPPER_BOUND = 40;
+
+ /**
+ * This is the upper boundary of level of level 2. (40%, 60%], and the lower boundary of level 3
+ * (60%, 80%].
+ */
+ private static final int SIGNAL_STRENGTH_2_OF_4_UPPER_BOUND = 60;
+
+ /**
+ * This is the upper boundary of level of level 3 (60%, 80%], and the lower boundary of level 4
+ * (80%, 100%].
+ */
+ private static final int SIGNAL_STRENGTH_3_OF_4_UPPER_BOUND = 80;
+
private final AnimatorListenerAdapter mResizeAnimatorListener =
new AnimatorListenerAdapter() {
@Override
@@ -172,7 +222,14 @@ public class ChannelBannerView extends FrameLayout
public ChannelBannerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mResources = getResources();
- mMainActivity = (MainActivity) context;
+
+ @SuppressWarnings("unchecked") // injection
+ MySingletons singletons = HasSingletons.get(MySingletons.class, context);
+ mCurrentChannelProvider = singletons.getCurrentChannelProvider();
+ mCurrentProgramProvider = singletons.getCurrentProgramProvider();
+ mCurrentPlayingPositionProvider = singletons.getCurrentPlayingPositionProvider();
+ mTvInputManagerHelper = singletons.getTvInputManagerHelperSingleton();
+ mTvOverlayManager = singletons.getOverlayManagerProvider();
mShowDurationMillis = mResources.getInteger(R.integer.channel_banner_show_duration);
mChannelLogoImageViewWidth =
@@ -195,20 +252,17 @@ public class ChannelBannerView extends FrameLayout
mProgramDescriptionFadeInAnimator =
AnimatorInflater.loadAnimator(
- mMainActivity, R.animator.channel_banner_program_description_fade_in);
+ context, R.animator.channel_banner_program_description_fade_in);
mProgramDescriptionFadeOutAnimator =
AnimatorInflater.loadAnimator(
- mMainActivity, R.animator.channel_banner_program_description_fade_out);
+ context, R.animator.channel_banner_program_description_fade_out);
- if (CommonFeatures.DVR.isEnabled(mMainActivity)) {
- mDvrManager = TvSingletons.getSingletons(mMainActivity).getDvrManager();
+ if (CommonFeatures.DVR.isEnabled(context)) {
+ mDvrManager = singletons.getDvrManagerSingleton();
} else {
mDvrManager = null;
}
- mContentRatingsManager =
- TvSingletons.getSingletons(getContext())
- .getTvInputManagerHelper()
- .getContentRatingsManager();
+ mContentRatingsManager = mTvInputManagerHelper.getContentRatingsManager();
mNoProgram =
new Program.Builder()
@@ -234,22 +288,23 @@ public class ChannelBannerView extends FrameLayout
mChannelView = findViewById(R.id.channel_banner_view);
- mChannelNumberTextView = (TextView) findViewById(R.id.channel_number);
- mChannelLogoImageView = (ImageView) findViewById(R.id.channel_logo);
- mProgramTextView = (TextView) findViewById(R.id.program_text);
- mTvInputLogoImageView = (ImageView) findViewById(R.id.tvinput_logo);
- mChannelNameTextView = (TextView) findViewById(R.id.channel_name);
- mProgramTimeTextView = (TextView) findViewById(R.id.program_time_text);
- mRemainingTimeView = (ProgressBar) findViewById(R.id.remaining_time);
- mRecordingIndicatorView = (TextView) findViewById(R.id.recording_indicator);
- mClosedCaptionTextView = (TextView) findViewById(R.id.closed_caption);
- mAspectRatioTextView = (TextView) findViewById(R.id.aspect_ratio);
- mResolutionTextView = (TextView) findViewById(R.id.resolution);
- mAudioChannelTextView = (TextView) findViewById(R.id.audio_channel);
- mContentRatingsTextViews[0] = (TextView) findViewById(R.id.content_ratings_0);
- mContentRatingsTextViews[1] = (TextView) findViewById(R.id.content_ratings_1);
- mContentRatingsTextViews[2] = (TextView) findViewById(R.id.content_ratings_2);
- mProgramDescriptionTextView = (TextView) findViewById(R.id.program_description);
+ mChannelNumberTextView = findViewById(R.id.channel_number);
+ mChannelLogoImageView = findViewById(R.id.channel_logo);
+ mProgramTextView = findViewById(R.id.program_text);
+ mTvInputLogoImageView = findViewById(R.id.tvinput_logo);
+ mChannelSignalStrengthView = findViewById(R.id.channel_signal_strength);
+ mChannelNameTextView = findViewById(R.id.channel_name);
+ mProgramTimeTextView = findViewById(R.id.program_time_text);
+ mRemainingTimeView = findViewById(R.id.remaining_time);
+ mRecordingIndicatorView = findViewById(R.id.recording_indicator);
+ mClosedCaptionTextView = findViewById(R.id.closed_caption);
+ mAspectRatioTextView = findViewById(R.id.aspect_ratio);
+ mResolutionTextView = findViewById(R.id.resolution);
+ mAudioChannelTextView = findViewById(R.id.audio_channel);
+ mContentRatingsTextViews[0] = findViewById(R.id.content_ratings_0);
+ mContentRatingsTextViews[1] = findViewById(R.id.content_ratings_1);
+ mContentRatingsTextViews[2] = findViewById(R.id.content_ratings_2);
+ mProgramDescriptionTextView = findViewById(R.id.program_description);
mAnchorView = findViewById(R.id.anchor);
mProgramDescriptionFadeInAnimator.setTarget(mProgramDescriptionTextView);
@@ -310,7 +365,7 @@ public class ChannelBannerView extends FrameLayout
*/
public void setBlockingContentRating(TvContentRating rating) {
mBlockingContentRating = rating;
- updateProgramRatings(mMainActivity.getCurrentProgram());
+ updateProgramRatings(mCurrentProgramProvider.get());
}
/**
@@ -328,20 +383,20 @@ public class ChannelBannerView extends FrameLayout
mAutoHideScheduler.schedule(mShowDurationMillis);
}
mBlockingContentRating = null;
- mCurrentChannel = mMainActivity.getCurrentChannel();
+ mCurrentChannel = mCurrentChannelProvider.get();
mCurrentChannelLogoExists =
mCurrentChannel != null && mCurrentChannel.channelLogoExists();
updateStreamInfo(null);
updateChannelInfo();
}
- updateProgramInfo(mMainActivity.getCurrentProgram());
+ updateProgramInfo(mCurrentProgramProvider.get());
mUpdateOnTune = false;
}
private void hide() {
mCurrentHeight = 0;
- mMainActivity
- .getOverlayManager()
+ mTvOverlayManager
+ .get()
.hideOverlays(
TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
@@ -367,10 +422,10 @@ public class ChannelBannerView extends FrameLayout
updateText(
mResolutionTextView,
Utils.getVideoDefinitionLevelString(
- mMainActivity, info.getVideoDefinitionLevel()));
+ getContext(), info.getVideoDefinitionLevel()));
updateText(
mAudioChannelTextView,
- Utils.getAudioChannelString(mMainActivity, info.getAudioChannelCount()));
+ Utils.getAudioChannelString(getContext(), info.getAudioChannelCount()));
} else {
// Channel change has been requested. But, StreamInfo hasn't been updated yet.
mClosedCaptionTextView.setVisibility(View.GONE);
@@ -418,8 +473,7 @@ public class ChannelBannerView extends FrameLayout
}
mChannelNumberTextView.setText(displayNumber);
mChannelNameTextView.setText(displayName);
- TvInputInfo info =
- mMainActivity.getTvInputManagerHelper().getTvInputInfo(getCurrentInputId());
+ TvInputInfo info = mTvInputManagerHelper.getTvInputInfo(getCurrentInputId());
if (info == null
|| !ImageLoader.loadBitmap(
createTvInputLogoLoaderCallback(info, this),
@@ -440,7 +494,7 @@ public class ChannelBannerView extends FrameLayout
}
private String getCurrentInputId() {
- Channel channel = mMainActivity.getCurrentChannel();
+ Channel channel = mCurrentChannelProvider.get();
return channel != null ? channel.getInputId() : null;
}
@@ -503,6 +557,34 @@ public class ChannelBannerView extends FrameLayout
};
}
+ public void updateChannelSignalStrengthView(int value) {
+ int resId = signalStrenghtToResId(value);
+ if (resId != 0) {
+ mChannelSignalStrengthView.setVisibility(View.VISIBLE);
+ mChannelSignalStrengthView.setImageResource(resId);
+ } else {
+ mChannelSignalStrengthView.setVisibility(View.GONE);
+ }
+ }
+
+ private int signalStrenghtToResId(int value) {
+ int signal = 0;
+ if (value >= 0 && value <= 100) {
+ if (value <= SIGNAL_STRENGTH_0_OF_4_UPPER_BOUND) {
+ signal = R.drawable.quantum_ic_signal_cellular_0_bar_white_24;
+ } else if (value <= SIGNAL_STRENGTH_1_OF_4_UPPER_BOUND) {
+ signal = R.drawable.quantum_ic_signal_cellular_1_bar_white_24;
+ } else if (value <= SIGNAL_STRENGTH_2_OF_4_UPPER_BOUND) {
+ signal = R.drawable.quantum_ic_signal_cellular_2_bar_white_24;
+ } else if (value <= SIGNAL_STRENGTH_3_OF_4_UPPER_BOUND) {
+ signal = R.drawable.quantum_ic_signal_cellular_3_bar_white_24;
+ } else {
+ signal = R.drawable.quantum_ic_signal_cellular_4_bar_white_24;
+ }
+ }
+ return signal;
+ }
+
private void updateLogo(@Nullable Bitmap logo) {
if (logo == null) {
// Need to update the text size of the program text view depending on the channel logo.
@@ -651,13 +733,14 @@ public class ChannelBannerView extends FrameLayout
mContentRatingsTextViews[i].setVisibility(View.GONE);
}
} else {
- TvContentRating[] ratings = (program == null) ? null : program.getContentRatings();
+ ImmutableList<TvContentRating> ratings =
+ (program == null) ? null : program.getContentRatings();
for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) {
- if (ratings == null || ratings.length <= i) {
+ if (ratings == null || ratings.size() <= i) {
mContentRatingsTextViews[i].setVisibility(View.GONE);
} else {
mContentRatingsTextViews[i].setText(
- mContentRatingsManager.getDisplayNameForRating(ratings[i]));
+ mContentRatingsManager.getDisplayNameForRating(ratings.get(i)));
mContentRatingsTextViews[i].setVisibility(View.VISIBLE);
}
}
@@ -667,13 +750,11 @@ public class ChannelBannerView extends FrameLayout
private void updateProgramTimeInfo(Program program) {
long durationMs = program.getDurationMillis();
long startTimeMs = program.getStartTimeUtcMillis();
- long endTimeMs = program.getEndTimeUtcMillis();
if (mLockType != LOCK_CHANNEL_INFO && durationMs > 0 && startTimeMs > 0) {
mProgramTimeTextView.setVisibility(View.VISIBLE);
mRemainingTimeView.setVisibility(View.VISIBLE);
- mProgramTimeTextView.setText(
- Utils.getDurationString(getContext(), startTimeMs, endTimeMs, true));
+ mProgramTimeTextView.setText(program.getDurationString(getContext()));
} else {
mProgramTimeTextView.setVisibility(View.GONE);
mRemainingTimeView.setVisibility(View.GONE);
@@ -713,7 +794,7 @@ public class ChannelBannerView extends FrameLayout
Program program, @Nullable ScheduledRecording recording) {
long programStartTime = program.getStartTimeUtcMillis();
long programEndTime = program.getEndTimeUtcMillis();
- long currentPosition = mMainActivity.getCurrentPlayingPosition();
+ long currentPosition = mCurrentPlayingPositionProvider.get();
updateRecordingIndicator(recording);
if (recording != null) {
// Recording now. Use recording-style progress bar.
@@ -734,12 +815,12 @@ public class ChannelBannerView extends FrameLayout
if (recording != null) {
if (mRemainingTimeView.getVisibility() == View.GONE) {
mRecordingIndicatorView.setText(
- mMainActivity
+ getContext()
.getResources()
.getString(
R.string.dvr_recording_till_format,
DateUtils.formatDateTime(
- mMainActivity,
+ getContext(),
recording.getEndTimeMs(),
DateUtils.FORMAT_SHOW_TIME)));
mRecordingIndicatorView.setCompoundDrawablePadding(mRecordingIconPadding);
@@ -754,7 +835,7 @@ public class ChannelBannerView extends FrameLayout
}
private boolean isCurrentProgram(ScheduledRecording recording, Program program) {
- long currentPosition = mMainActivity.getCurrentPlayingPosition();
+ long currentPosition = mCurrentPlayingPositionProvider.get();
return (recording.getType() == ScheduledRecording.TYPE_PROGRAM
&& recording.getProgramId() == program.getId())
|| (recording.getType() == ScheduledRecording.TYPE_TIMED
diff --git a/src/com/android/tv/ui/DetailsActivity.java b/src/com/android/tv/ui/DetailsActivity.java
new file mode 100644
index 00000000..80c0f64b
--- /dev/null
+++ b/src/com/android/tv/ui/DetailsActivity.java
@@ -0,0 +1,209 @@
+/*
+ * 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.Activity;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.app.DetailsFragment;
+import android.transition.Transition;
+import android.transition.Transition.TransitionListener;
+import android.util.Log;
+import android.view.View;
+import com.android.tv.R;
+import com.android.tv.Starter;
+import com.android.tv.TvSingletons;
+import com.android.tv.dialog.PinDialogFragment;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.ui.browse.CurrentRecordingDetailsFragment;
+import com.android.tv.dvr.ui.browse.RecordedProgramDetailsFragment;
+import com.android.tv.dvr.ui.browse.ScheduledRecordingDetailsFragment;
+import com.android.tv.dvr.ui.browse.SeriesRecordingDetailsFragment;
+
+/** Activity to show details view. */
+public class DetailsActivity extends Activity implements PinDialogFragment.OnPinCheckedListener {
+ private static final String TAG = "DetailsActivity";
+
+ private static final long INVALID_RECORD_ID = -1;
+
+ /** Name of record id added to the Intent. */
+ public static final String RECORDING_ID = "record_id";
+ /** Name of program uri added to the Intent. */
+ public static final String PROGRAM = "program";
+ /** Name of channel id added to the Intent. */
+ public static final String CHANNEL_ID = "channel_id";
+ /** Name of input id added to the Intent. */
+ public static final String INPUT_ID = "input_id";
+
+ /**
+ * Name of flag added to the Intent to determine if details view should hide "View schedule"
+ * button.
+ */
+ public static final String HIDE_VIEW_SCHEDULE = "hide_view_schedule";
+
+ /** Name of details view's type added to the intent. */
+ public static final String DETAILS_VIEW_TYPE = "details_view_type";
+
+ /** Name of shared element between activities. */
+ public static final String SHARED_ELEMENT_NAME = "shared_element";
+
+ /** CURRENT_RECORDING_VIEW refers to Current Recordings in DVR. */
+ public static final int CURRENT_RECORDING_VIEW = 1;
+
+ /** SCHEDULED_RECORDING_VIEW refers to Scheduled Recordings in DVR. */
+ public static final int SCHEDULED_RECORDING_VIEW = 2;
+
+ /** RECORDED_PROGRAM_VIEW refers to Recorded programs in DVR. */
+ public static final int RECORDED_PROGRAM_VIEW = 3;
+
+ /** SERIES_RECORDING_VIEW refers to series recording in DVR. */
+ public static final int SERIES_RECORDING_VIEW = 4;
+
+ /** SERIES_RECORDING_VIEW refers to program. */
+ public static final int PROGRAM_VIEW = 5;
+
+ public static final int REQUEST_DELETE = 1;
+
+ private PinDialogFragment.OnPinCheckedListener mOnPinCheckedListener;
+ private long mRecordId = INVALID_RECORD_ID;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Starter.start(this);
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_dvr_details);
+ long recordId = getIntent().getLongExtra(RECORDING_ID, INVALID_RECORD_ID);
+ int detailsViewType = getIntent().getIntExtra(DETAILS_VIEW_TYPE, -1);
+ boolean hideViewSchedule = getIntent().getBooleanExtra(HIDE_VIEW_SCHEDULE, false);
+ long channelId = getIntent().getLongExtra(CHANNEL_ID, -1);
+ DetailsFragment detailsFragment = null;
+ Bundle args = new Bundle();
+ if (detailsViewType != -1 && savedInstanceState == null) {
+ if (recordId != INVALID_RECORD_ID) {
+ mRecordId = recordId;
+ args.putLong(RECORDING_ID, mRecordId);
+ if (detailsViewType == CURRENT_RECORDING_VIEW) {
+ detailsFragment = new CurrentRecordingDetailsFragment();
+ } else if (detailsViewType == SCHEDULED_RECORDING_VIEW) {
+ args.putBoolean(HIDE_VIEW_SCHEDULE, hideViewSchedule);
+ detailsFragment = new ScheduledRecordingDetailsFragment();
+ } else if (detailsViewType == RECORDED_PROGRAM_VIEW) {
+ detailsFragment = new RecordedProgramDetailsFragment();
+ } else if (detailsViewType == SERIES_RECORDING_VIEW) {
+ detailsFragment = new SeriesRecordingDetailsFragment();
+ }
+ } else if (detailsViewType == PROGRAM_VIEW && channelId != -1) {
+ Parcelable program = getIntent().getParcelableExtra(PROGRAM);
+ if (program != null) {
+ args.putLong(CHANNEL_ID, channelId);
+ args.putParcelable(PROGRAM, program);
+ args.putString(INPUT_ID, getIntent().getStringExtra(INPUT_ID));
+ detailsFragment = new ProgramDetailsFragment();
+ }
+ }
+ if (detailsFragment != null) {
+ detailsFragment.setArguments(args);
+ getFragmentManager()
+ .beginTransaction()
+ .replace(R.id.dvr_details_view_frame, detailsFragment)
+ .commit();
+ }
+ }
+
+ // This is a workaround for the focus on O device
+ addTransitionListener();
+ }
+
+ @Override
+ public void onPinChecked(boolean checked, int type, String rating) {
+ if (mOnPinCheckedListener != null) {
+ mOnPinCheckedListener.onPinChecked(checked, type, rating);
+ }
+ }
+
+ public void setOnPinCheckListener(PinDialogFragment.OnPinCheckedListener listener) {
+ mOnPinCheckedListener = listener;
+ }
+
+ private void addTransitionListener() {
+ getWindow()
+ .getSharedElementEnterTransition()
+ .addListener(
+ new TransitionListener() {
+ @Override
+ public void onTransitionStart(Transition transition) {
+ // Do nothing
+ }
+
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ View actions = findViewById(R.id.details_overview_actions);
+ if (actions != null) {
+ actions.requestFocus();
+ }
+ }
+
+ @Override
+ public void onTransitionCancel(Transition transition) {
+ // Do nothing
+
+ }
+
+ @Override
+ public void onTransitionPause(Transition transition) {
+ // Do nothing
+ }
+
+ @Override
+ public void onTransitionResume(Transition transition) {
+ // Do nothing
+ }
+ });
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ switch (requestCode) {
+ case REQUEST_DELETE:
+ // If request is cancelled, the result arrays are empty.
+ if (grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ delete(true);
+ } else {
+ Log.i(
+ TAG,
+ "Write permission denied, Not trying to delete the file for "
+ + mRecordId);
+ delete(false);
+ }
+ break;
+ default:
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
+ private void delete(boolean deleteFile) {
+ if (mRecordId != INVALID_RECORD_ID) {
+ DvrManager dvrManager = TvSingletons.getSingletons(this).getDvrManager();
+ dvrManager.removeRecordedProgram(mRecordId, deleteFile);
+ }
+ finish();
+ }
+}
diff --git a/src/com/android/tv/ui/FullscreenDialogView.java b/src/com/android/tv/ui/FullscreenDialogView.java
index 800fa85a..d3fec824 100644
--- a/src/com/android/tv/ui/FullscreenDialogView.java
+++ b/src/com/android/tv/ui/FullscreenDialogView.java
@@ -83,13 +83,7 @@ public class FullscreenDialogView extends FrameLayout
/** Dismisses the host {@link Dialog}. */
protected void dismiss() {
- startExitAnimation(
- new Runnable() {
- @Override
- public void run() {
- mDialog.dismiss();
- }
- });
+ startExitAnimation(() -> mDialog.dismiss());
}
@Override
@@ -110,9 +104,7 @@ public class FullscreenDialogView extends FrameLayout
v.mSkipEnterAlphaAnimation = true;
v.initialize(mActivity, mDialog);
startExitAnimation(
- new Runnable() {
- @Override
- public void run() {
+ () ->
new Handler()
.postDelayed(
new Runnable() {
@@ -122,9 +114,7 @@ public class FullscreenDialogView extends FrameLayout
getDialog().setContentView(v);
}
},
- TRANSITION_INTERVAL_MS);
- }
- });
+ TRANSITION_INTERVAL_MS));
}
/** Called when an enter animation starts. Sub-view specific animation can be implemented. */
diff --git a/src/com/android/tv/ui/InputBannerView.java b/src/com/android/tv/ui/InputBannerView.java
index 5ac715bf..d0609186 100644
--- a/src/com/android/tv/ui/InputBannerView.java
+++ b/src/com/android/tv/ui/InputBannerView.java
@@ -31,9 +31,7 @@ public class InputBannerView extends LinearLayout implements TvTransitionManager
private final long mShowDurationMillis;
private final Runnable mHideRunnable =
- new Runnable() {
- @Override
- public void run() {
+ () ->
((MainActivity) getContext())
.getOverlayManager()
.hideOverlays(
@@ -42,9 +40,6 @@ public class InputBannerView extends LinearLayout implements TvTransitionManager
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
- }
- };
-
private TextView mInputLabelTextView;
private TextView mSecondaryInputLabelTextView;
diff --git a/src/com/android/tv/ui/IntroView.java b/src/com/android/tv/ui/IntroView.java
index be9fb691..e7240747 100644
--- a/src/com/android/tv/ui/IntroView.java
+++ b/src/com/android/tv/ui/IntroView.java
@@ -102,13 +102,7 @@ public class IntroView extends FullscreenDialogView {
.setInterpolator(interpolator)
.setDuration(duration)
.withLayer()
- .withEndAction(
- new Runnable() {
- @Override
- public void run() {
- onAnimationEnded.run();
- }
- })
+ .withEndAction(onAnimationEnded)
.start();
}
}
diff --git a/src/com/android/tv/ui/KeypadChannelSwitchView.java b/src/com/android/tv/ui/KeypadChannelSwitchView.java
index e2625811..a26175a4 100644
--- a/src/com/android/tv/ui/KeypadChannelSwitchView.java
+++ b/src/com/android/tv/ui/KeypadChannelSwitchView.java
@@ -148,13 +148,10 @@ public class KeypadChannelSwitchView extends LinearLayout
mChannelItemListView.setFocusable(false);
final Channel channel = ((Channel) mAdapter.getItem(position));
postDelayed(
- new Runnable() {
- @Override
- public void run() {
- mChannelItemListView.setFocusable(true);
- mMainActivity.tuneToChannel(channel);
- mTracker.sendChannelNumberItemClicked();
- }
+ () -> {
+ mChannelItemListView.setFocusable(true);
+ mMainActivity.tuneToChannel(channel);
+ mTracker.sendChannelNumberItemClicked();
},
mRippleAnimDurationMillis);
}
diff --git a/src/com/android/tv/ui/ProgramDetailsFragment.java b/src/com/android/tv/ui/ProgramDetailsFragment.java
new file mode 100644
index 00000000..88a7b2ca
--- /dev/null
+++ b/src/com/android/tv/ui/ProgramDetailsFragment.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2018 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.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.app.DetailsFragment;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.support.v17.leanback.widget.DetailsOverviewRow;
+import android.support.v17.leanback.widget.DetailsOverviewRowPresenter;
+import android.support.v17.leanback.widget.OnActionClickedListener;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.text.TextUtils;
+import com.android.tv.R;
+import com.android.tv.TvSingletons;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.data.Program;
+import com.android.tv.data.api.Channel;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrScheduleManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.dvr.ui.browse.ActionPresenterSelector;
+import com.android.tv.dvr.ui.browse.DetailsContent;
+import com.android.tv.dvr.ui.browse.DetailsContentPresenter;
+import com.android.tv.dvr.ui.browse.DetailsViewBackgroundHelper;
+import com.android.tv.util.images.ImageLoader;
+
+/** A fragment shows the details of a Program */
+public class ProgramDetailsFragment extends DetailsFragment
+ implements DvrDataManager.ScheduledRecordingListener,
+ DvrScheduleManager.OnConflictStateChangeListener {
+ private static final int LOAD_LOGO_IMAGE = 1;
+ private static final int LOAD_BACKGROUND_IMAGE = 2;
+
+ private static final int ACTION_VIEW_SCHEDULE = 1;
+ private static final int ACTION_CANCEL = 2;
+ private static final int ACTION_SCHEDULE_RECORDING = 3;
+
+ protected DetailsViewBackgroundHelper mBackgroundHelper;
+ private ArrayObjectAdapter mRowsAdapter;
+ private DetailsOverviewRow mDetailsOverview;
+ private Program mProgram;
+ private String mInputId;
+ private ScheduledRecording mScheduledRecording;
+ private DvrManager mDvrManager;
+ private DvrDataManager mDvrDataManager;
+ private DvrScheduleManager mDvrScheduleManager;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (!onLoadDetails(getArguments())) {
+ getActivity().finish();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ mDvrDataManager.removeScheduledRecordingListener(this);
+ mDvrScheduleManager.removeOnConflictStateChangeListener(this);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ VerticalGridView container =
+ (VerticalGridView) getActivity().findViewById(R.id.container_list);
+ // Need to manually modify offset. Please refer DetailsFragment.setVerticalGridViewLayout.
+ container.setItemAlignmentOffset(0);
+ container.setWindowAlignmentOffset(
+ getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top));
+ }
+
+ private void setupAdapter() {
+ DetailsOverviewRowPresenter rowPresenter =
+ new DetailsOverviewRowPresenter(new DetailsContentPresenter(getActivity()));
+ rowPresenter.setBackgroundColor(
+ getResources().getColor(R.color.common_tv_background, null));
+ rowPresenter.setSharedElementEnterTransition(
+ getActivity(), DetailsActivity.SHARED_ELEMENT_NAME);
+ rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener());
+ mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter));
+ setAdapter(mRowsAdapter);
+ }
+
+ /** Sets details overview. */
+ protected void setDetailsOverviewRow(DetailsContent detailsContent) {
+ mDetailsOverview = new DetailsOverviewRow(detailsContent);
+ mDetailsOverview.setActionsAdapter(onCreateActionsAdapter());
+ mRowsAdapter.add(mDetailsOverview);
+ onLoadLogoAndBackgroundImages(detailsContent);
+ }
+
+ /** Creates and returns presenter selector will be used by rows adaptor. */
+ protected PresenterSelector onCreatePresenterSelector(
+ DetailsOverviewRowPresenter rowPresenter) {
+ ClassPresenterSelector presenterSelector = new ClassPresenterSelector();
+ presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter);
+ return presenterSelector;
+ }
+
+ /** Updates actions of details overview. */
+ protected void updateActions() {
+ mDetailsOverview.setActionsAdapter(onCreateActionsAdapter());
+ }
+
+ /**
+ * Loads program details according to the arguments the fragment got.
+ *
+ * @return false if cannot find valid programs, else return true. If the return value is false,
+ * the detail activity and fragment will be ended.
+ */
+ private boolean onLoadDetails(Bundle args) {
+ Program program = args.getParcelable(DetailsActivity.PROGRAM);
+ long channelId = args.getLong(DetailsActivity.CHANNEL_ID);
+ String inputId = args.getString(DetailsActivity.INPUT_ID);
+ if (program != null && channelId != Channel.INVALID_ID && !TextUtils.isEmpty(inputId)) {
+ mProgram = program;
+ mInputId = inputId;
+ TvSingletons singletons = TvSingletons.getSingletons(getContext());
+ mDvrDataManager = singletons.getDvrDataManager();
+ mDvrManager = singletons.getDvrManager();
+ mDvrScheduleManager = singletons.getDvrScheduleManager();
+ mScheduledRecording =
+ mDvrDataManager.getScheduledRecordingForProgramId(program.getId());
+ mBackgroundHelper = new DetailsViewBackgroundHelper(getActivity());
+ setupAdapter();
+ setDetailsOverviewRow(DetailsContent.createFromProgram(getContext(), mProgram));
+ mDvrDataManager.addScheduledRecordingListener(this);
+ mDvrScheduleManager.addOnConflictStateChangeListener(this);
+ return true;
+ }
+ return false;
+ }
+
+ private int getScheduleIconId() {
+ if (mDvrManager.isConflicting(mScheduledRecording)) {
+ return R.drawable.ic_warning_white_32dp;
+ } else {
+ return R.drawable.ic_schedule_32dp;
+ }
+ }
+
+ /** Creates actions users can interact with and their adaptor for this fragment. */
+ private SparseArrayObjectAdapter onCreateActionsAdapter() {
+ SparseArrayObjectAdapter adapter =
+ new SparseArrayObjectAdapter(new ActionPresenterSelector());
+ Resources res = getResources();
+ if (mScheduledRecording != null) {
+ adapter.set(
+ ACTION_VIEW_SCHEDULE,
+ new Action(
+ ACTION_VIEW_SCHEDULE,
+ res.getString(R.string.dvr_detail_view_schedule),
+ null,
+ res.getDrawable(getScheduleIconId())));
+ adapter.set(
+ ACTION_CANCEL,
+ new Action(
+ ACTION_CANCEL,
+ res.getString(R.string.dvr_detail_cancel_recording),
+ null,
+ res.getDrawable(R.drawable.ic_dvr_cancel_32dp)));
+ } else if (CommonFeatures.DVR.isEnabled(getActivity())
+ && mDvrManager.isProgramRecordable(mProgram)) {
+ adapter.set(
+ ACTION_SCHEDULE_RECORDING,
+ new Action(
+ ACTION_SCHEDULE_RECORDING,
+ res.getString(R.string.dvr_detail_schedule_recording),
+ null,
+ res.getDrawable(R.drawable.ic_schedule_32dp)));
+ }
+ return adapter;
+ }
+
+ /**
+ * Creates actions listeners to implement the behavior of the fragment after users click some
+ * action buttons.
+ */
+ private OnActionClickedListener onCreateOnActionClickedListener() {
+ return new OnActionClickedListener() {
+ @Override
+ public void onActionClicked(Action action) {
+ long actionId = action.getId();
+ if (actionId == ACTION_VIEW_SCHEDULE) {
+ DvrUiHelper.startSchedulesActivity(getContext(), mScheduledRecording);
+ } else if (actionId == ACTION_CANCEL) {
+ mDvrManager.removeScheduledRecording(mScheduledRecording);
+ } else if (actionId == ACTION_SCHEDULE_RECORDING) {
+ DvrUiHelper.checkStorageStatusAndShowErrorMessage(
+ getActivity(),
+ mInputId,
+ () ->
+ DvrUiHelper.requestRecordingFutureProgram(
+ getActivity(), mProgram, false));
+ }
+ }
+ };
+ }
+
+ /** Loads logo and background images for detail fragments. */
+ protected void onLoadLogoAndBackgroundImages(DetailsContent detailsContent) {
+ Drawable logoDrawable = null;
+ Drawable backgroundDrawable = null;
+ if (TextUtils.isEmpty(detailsContent.getLogoImageUri())) {
+ logoDrawable =
+ getContext().getResources().getDrawable(R.drawable.dvr_default_poster, null);
+ mDetailsOverview.setImageDrawable(logoDrawable);
+ }
+ if (TextUtils.isEmpty(detailsContent.getBackgroundImageUri())) {
+ backgroundDrawable =
+ getContext().getResources().getDrawable(R.drawable.dvr_default_poster, null);
+ mBackgroundHelper.setBackground(backgroundDrawable);
+ }
+ if (logoDrawable != null && backgroundDrawable != null) {
+ return;
+ }
+ if (logoDrawable == null
+ && backgroundDrawable == null
+ && detailsContent
+ .getLogoImageUri()
+ .equals(detailsContent.getBackgroundImageUri())) {
+ ImageLoader.loadBitmap(
+ getContext(),
+ detailsContent.getLogoImageUri(),
+ new MyImageLoaderCallback(
+ this, LOAD_LOGO_IMAGE | LOAD_BACKGROUND_IMAGE, getContext()));
+ return;
+ }
+ if (logoDrawable == null) {
+ int imageWidth = getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_width);
+ int imageHeight =
+ getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_height);
+ ImageLoader.loadBitmap(
+ getContext(),
+ detailsContent.getLogoImageUri(),
+ imageWidth,
+ imageHeight,
+ new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE, getContext()));
+ }
+ if (backgroundDrawable == null) {
+ ImageLoader.loadBitmap(
+ getContext(),
+ detailsContent.getBackgroundImageUri(),
+ new MyImageLoaderCallback(this, LOAD_BACKGROUND_IMAGE, getContext()));
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecording recording : scheduledRecordings) {
+ if (recording.getProgramId() == mProgram.getId()) {
+ mScheduledRecording = recording;
+ updateActions();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
+ if (mScheduledRecording == null) {
+ return;
+ }
+ for (ScheduledRecording recording : scheduledRecordings) {
+ if (recording.getId() == mScheduledRecording.getId()) {
+ mScheduledRecording = null;
+ updateActions();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
+ if (mScheduledRecording == null) {
+ return;
+ }
+ for (ScheduledRecording recording : scheduledRecordings) {
+ if (recording.getId() == mScheduledRecording.getId()) {
+ mScheduledRecording = recording;
+ updateActions();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onConflictStateChange(boolean conflict, ScheduledRecording... scheduledRecordings) {
+ onScheduledRecordingStatusChanged(scheduledRecordings);
+ }
+
+ private static class MyImageLoaderCallback
+ extends ImageLoader.ImageLoaderCallback<ProgramDetailsFragment> {
+ private final Context mContext;
+ private final int mLoadType;
+
+ public MyImageLoaderCallback(
+ ProgramDetailsFragment fragment, int loadType, Context context) {
+ super(fragment);
+ mLoadType = loadType;
+ mContext = context;
+ }
+
+ @Override
+ public void onBitmapLoaded(ProgramDetailsFragment fragment, @Nullable Bitmap bitmap) {
+ Drawable drawable;
+ int loadType = mLoadType;
+ if (bitmap == null) {
+ Resources res = mContext.getResources();
+ drawable = res.getDrawable(R.drawable.dvr_default_poster, null);
+ if ((loadType & LOAD_BACKGROUND_IMAGE) != 0 && !fragment.isDetached()) {
+ loadType &= ~LOAD_BACKGROUND_IMAGE;
+ fragment.mBackgroundHelper.setBackgroundColor(
+ res.getColor(R.color.dvr_detail_default_background));
+ fragment.mBackgroundHelper.setScrim(
+ res.getColor(R.color.dvr_detail_default_background_scrim));
+ }
+ } else {
+ drawable = new BitmapDrawable(mContext.getResources(), bitmap);
+ }
+ if (!fragment.isDetached()) {
+ if ((loadType & LOAD_LOGO_IMAGE) != 0) {
+ fragment.mDetailsOverview.setImageDrawable(drawable);
+ }
+ if ((loadType & LOAD_BACKGROUND_IMAGE) != 0) {
+ fragment.mBackgroundHelper.setBackground(drawable);
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java
index bb98d974..5ac6bd83 100644
--- a/src/com/android/tv/ui/TunableTvView.java
+++ b/src/com/android/tv/ui/TunableTvView.java
@@ -20,11 +20,7 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.ApplicationErrorReport;
import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -38,7 +34,6 @@ 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;
@@ -47,6 +42,7 @@ import android.os.Bundle;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.AttributeSet;
@@ -55,16 +51,17 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.View;
+import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.android.tv.InputSessionManager;
import com.android.tv.InputSessionManager.TvViewSession;
import com.android.tv.R;
-import com.android.tv.TvFeatures;
import com.android.tv.TvSingletons;
import com.android.tv.analytics.Tracker;
-import com.android.tv.common.BuildConfig;
import com.android.tv.common.CommonConstants;
+import com.android.tv.common.compat.TvInputConstantCompat;
+import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.common.util.Debug;
@@ -75,9 +72,11 @@ import com.android.tv.data.ProgramDataManager;
import com.android.tv.data.StreamInfo;
import com.android.tv.data.WatchedHistoryManager;
import com.android.tv.data.api.Channel;
+import com.android.tv.features.TvFeatures;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.recommendation.NotificationService;
+import com.android.tv.ui.api.TunableTvViewPlayingApi;
import com.android.tv.util.NetworkUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -95,8 +94,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
public static final int VIDEO_UNAVAILABLE_REASON_NO_RESOURCE = -2;
public static final int VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED = -3;
public static final int VIDEO_UNAVAILABLE_REASON_NONE = -100;
-
- private OnTalkBackDpadKeyListener mOnTalkBackDpadKeyListener;
+ private final AccessibilityManager mAccessibilityManager;
@Retention(RetentionPolicy.SOURCE)
@IntDef({BLOCK_SCREEN_TYPE_NO_UI, BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW, BLOCK_SCREEN_TYPE_NORMAL})
@@ -132,7 +130,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
private AppLayerTvView mTvView;
private TvViewSession mTvViewSession;
- private Channel mCurrentChannel;
+ @Nullable private Channel mCurrentChannel;
private TvInputManagerHelper mInputManagerHelper;
private ContentRatingsManager mContentRatingsManager;
private ParentalControlSettings mParentalControlSettings;
@@ -190,8 +188,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
private final ConnectivityManager mConnectivityManager;
private final InputSessionManager mInputSessionManager;
- private final TvInputCallback mCallback =
- new TvInputCallback() {
+ private int mChannelSignalStrength;
+
+ private final TvInputCallbackCompat mCallback =
+ new TvInputCallbackCompat() {
@Override
public void onConnectionFailed(String inputId) {
Log.w(TAG, "Failed to bind an input");
@@ -252,7 +252,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
}
}
if (mOnTuneListener != null) {
- mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
+ mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true);
}
}
@@ -305,7 +305,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
}
}
if (mOnTuneListener != null) {
- mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
+ // should not change audio track automatically when an audio track or a
+ // subtitle track is selected
+ mOnTuneListener.onStreamInfoChanged(
+ TunableTvView.this, type == TvTrackInfo.TYPE_VIDEO);
}
}
@@ -316,60 +319,15 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
.log(
"Start up of Live TV ends,"
+ " TunableTvView.onVideoAvailable resets timer");
- long startUpDurationTime = Debug.getTimer(Debug.TAG_START_UP_TIMER).reset();
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).reset();
Debug.removeTimer(Debug.TAG_START_UP_TIMER);
- if (BuildConfig.ENG
- && startUpDurationTime > Debug.TIME_START_UP_DURATION_THRESHOLD) {
- showAlertDialogForLongStartUp();
- }
mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NONE;
updateBlockScreenAndMuting();
if (mOnTuneListener != null) {
- mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
+ mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true);
}
}
- private void showAlertDialogForLongStartUp() {
- new AlertDialog.Builder(getContext())
- .setTitle(getContext().getString(R.string.settings_send_feedback))
- .setMessage(
- "Because the start up time of Live channels is too long,"
- + " please send feedback")
- .setPositiveButton(
- android.R.string.ok,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(
- DialogInterface dialogInterface, int i) {
- Intent intent = new Intent(Intent.ACTION_APP_ERROR);
- ApplicationErrorReport report =
- new ApplicationErrorReport();
- report.packageName =
- report.processName =
- getContext()
- .getApplicationContext()
- .getPackageName();
- report.time = System.currentTimeMillis();
- report.type = ApplicationErrorReport.TYPE_CRASH;
-
- // Add the crash info to add title of feedback
- // automatically.
- ApplicationErrorReport.CrashInfo crash =
- new ApplicationErrorReport.CrashInfo();
- crash.exceptionClassName =
- "Live TV start up takes long time";
- crash.exceptionMessage =
- "The start up time of Live TV is too long";
- report.crashInfo = crash;
-
- intent.putExtra(Intent.EXTRA_BUG_REPORT, report);
- getContext().startActivity(intent);
- }
- })
- .setNegativeButton(android.R.string.cancel, null)
- .show();
- }
-
@Override
public void onVideoUnavailable(String inputId, int reason) {
if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING
@@ -390,12 +348,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
}
updateBlockScreenAndMuting();
if (mOnTuneListener != null) {
- mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
+ mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true);
}
switch (reason) {
case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
+ case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED:
mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason);
break;
default:
@@ -441,6 +400,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE;
setTimeShiftAvailable(available);
}
+
+ @Override
+ public void onSignalStrength(String inputId, int value) {
+ mChannelSignalStrength = value;
+ if (mOnTuneListener != null) {
+ mOnTuneListener.onChannelSignalStrength();
+ }
+ }
};
public TunableTvView(Context context) {
@@ -502,35 +469,12 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
}
}
});
- View placeholder = findViewById(R.id.placeholder);
- placeholder.requestFocus();
- findViewById(R.id.channel_up)
- .setOnFocusChangeListener(
- (v, hasFocus) -> {
- if (hasFocus) {
- placeholder.requestFocus();
- if (mOnTalkBackDpadKeyListener != null) {
- mOnTalkBackDpadKeyListener.onTalkBackDpadKey(
- KeyEvent.KEYCODE_DPAD_UP);
- }
- }
- });
- findViewById(R.id.channel_down)
- .setOnFocusChangeListener(
- (v, hasFocus) -> {
- if (hasFocus) {
- placeholder.requestFocus();
- if (mOnTalkBackDpadKeyListener != null) {
- mOnTalkBackDpadKeyListener.onTalkBackDpadKey(
- KeyEvent.KEYCODE_DPAD_DOWN);
- }
- }
- });
+ mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
}
public void initialize(
ProgramDataManager programDataManager, TvInputManagerHelper tvInputManagerHelper) {
- mTvView = (AppLayerTvView) findViewById(R.id.tv_view);
+ mTvView = findViewById(R.id.tv_view);
mProgramDataManager = programDataManager;
mInputManagerHelper = tvInputManagerHelper;
mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager();
@@ -621,6 +565,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
mIsUnderShrunken = isUnderShrunken;
}
+ public int getChannelSignalStrength() {
+ return mChannelSignalStrength;
+ }
+
+ public void resetChannelSignalStrength() {
+ mChannelSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED;
+ }
+
@Override
public boolean isPlaying() {
return mStarted;
@@ -714,12 +666,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
}
updateBlockScreenAndMuting();
if (mOnTuneListener != null) {
- mOnTuneListener.onStreamInfoChanged(this);
+ mOnTuneListener.onStreamInfoChanged(this, true);
}
return true;
}
@Override
+ @Nullable
public Channel getCurrentChannel() {
return mCurrentChannel;
}
@@ -795,13 +748,15 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
void onUnexpectedStop(Channel channel);
- void onStreamInfoChanged(StreamInfo info);
+ void onStreamInfoChanged(StreamInfo info, boolean allowAutoSelectionOfTrack);
void onChannelRetuned(Uri channel);
void onContentBlocked();
void onContentAllowed();
+
+ void onChannelSignalStrength();
}
public void unblockContent(TvContentRating rating) {
@@ -869,14 +824,15 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
mTvView.setOnUnhandledInputEventListener(listener);
}
- public void setOnTalkBackDpadKeyListener(OnTalkBackDpadKeyListener listener) {
- mOnTalkBackDpadKeyListener = listener;
- }
-
public void setClosedCaptionEnabled(boolean enabled) {
mTvView.setCaptionEnabled(enabled);
}
+ @VisibleForTesting
+ public void setOnTuneListener(OnTuneListener listener) {
+ mOnTuneListener = listener;
+ }
+
public List<TvTrackInfo> getTracks(int type) {
return mTvView.getTracks(type);
}
@@ -1044,6 +1000,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
if (text != null) {
mBlockScreenView.setInfoText(text);
}
+ mBlockScreenView.setInfoTextClickable(mScreenBlocked && mParentControlEnabled);
}
/**
@@ -1053,6 +1010,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
private String getBlockScreenText() {
// TODO: add a test for this method
Resources res = getResources();
+ boolean isA11y = mAccessibilityManager.isEnabled();
+
if (mScreenBlocked && mParentControlEnabled) {
switch (mBlockScreenType) {
case BLOCK_SCREEN_TYPE_NO_UI:
@@ -1060,7 +1019,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
return "";
case BLOCK_SCREEN_TYPE_NORMAL:
if (mCanModifyParentalControls) {
- return res.getString(R.string.tvview_channel_locked);
+ return res.getString(
+ isA11y
+ ? R.string.tvview_channel_locked_talkback
+ : R.string.tvview_channel_locked);
} else {
return res.getString(R.string.tvview_channel_locked_no_permission);
}
@@ -1081,15 +1043,26 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
case BLOCK_SCREEN_TYPE_NORMAL:
if (TextUtils.isEmpty(name)) {
if (mCanModifyParentalControls) {
- return res.getString(R.string.tvview_content_locked);
+ return res.getString(
+ isA11y
+ ? R.string.tvview_content_locked_talkback
+ : R.string.tvview_content_locked);
} else {
return res.getString(R.string.tvview_content_locked_no_permission);
}
} else {
if (mCanModifyParentalControls) {
return name.equals(res.getString(R.string.unrated_rating_name))
- ? res.getString(R.string.tvview_content_locked_unrated)
- : res.getString(R.string.tvview_content_locked_format, name);
+ ? res.getString(
+ isA11y
+ ? R.string
+ .tvview_content_locked_unrated_talkback
+ : R.string.tvview_content_locked_unrated)
+ : res.getString(
+ isA11y
+ ? R.string.tvview_content_locked_format_talkback
+ : R.string.tvview_content_locked_format,
+ name);
} else {
return name.equals(res.getString(R.string.unrated_rating_name))
? res.getString(
@@ -1106,6 +1079,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
return res.getString(R.string.tvview_msg_audio_only);
case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
return res.getString(R.string.tvview_msg_weak_signal);
+ case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED:
+ return res.getString(R.string.msg_channel_unavailable_not_connected);
case VIDEO_UNAVAILABLE_REASON_NO_RESOURCE:
return getTuneConflictMessage();
default:
@@ -1122,7 +1097,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
&& (mScreenBlocked
|| mBlockedContentRating != null
|| mVideoUnavailableReason
- == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN)) {
+ == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN
+ || mVideoUnavailableReason
+ == CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED)) {
((Activity) getContext()).finish();
return true;
}
@@ -1237,20 +1214,11 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
.setDuration(durationMillis)
.setInterpolator(interpolator)
.withStartAction(
- new Runnable() {
- @Override
- public void run() {
- mFadeState = FADING_OUT;
- mActionAfterFade = actionAfterFade;
- }
+ () -> {
+ mFadeState = FADING_OUT;
+ mActionAfterFade = actionAfterFade;
})
- .withEndAction(
- new Runnable() {
- @Override
- public void run() {
- mFadeState = FADED_OUT;
- }
- });
+ .withEndAction(() -> mFadeState = FADED_OUT);
}
/** Fade in this TunableTvView. Fade in by decreasing the dimming. */
@@ -1264,20 +1232,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
.setDuration(durationMillis)
.setInterpolator(interpolator)
.withStartAction(
- new Runnable() {
- @Override
- public void run() {
- mFadeState = FADING_IN;
- mActionAfterFade = actionAfterFade;
- }
+ () -> {
+ mFadeState = FADING_IN;
+ mActionAfterFade = actionAfterFade;
})
.withEndAction(
- new Runnable() {
- @Override
- public void run() {
- mFadeState = FADED_IN;
- mDimScreenView.setVisibility(View.GONE);
- }
+ () -> {
+ mFadeState = FADED_IN;
+ mDimScreenView.setVisibility(View.GONE);
});
}
@@ -1298,6 +1260,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
mTimeShiftListener = listener;
}
+ public void setBlockedInfoOnClickListener(@Nullable OnClickListener onClickListener) {
+ mBlockScreenView.setInfoTextOnClickListener(onClickListener);
+ }
+
private void setTimeShiftAvailable(boolean isTimeShiftAvailable) {
if (mTimeShiftAvailable == isTimeShiftAvailable) {
return;
@@ -1336,7 +1302,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
/** Plays the media, if the current input supports time-shifting. */
@Override
- public void timeshiftPlay() {
+ public void timeShiftPlay() {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
}
@@ -1348,7 +1314,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
/** Pauses the media, if the current input supports time-shifting. */
@Override
- public void timeshiftPause() {
+ public void timeShiftPause() {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
}
@@ -1364,7 +1330,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
* @param speed The speed to rewind the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x.
*/
@Override
- public void timeshiftRewind(int speed) {
+ public void timeShiftRewind(int speed) {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
} else {
@@ -1384,7 +1350,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
* @param speed The speed to forward the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x.
*/
@Override
- public void timeshiftFastForward(int speed) {
+ public void timeShiftFastForward(int speed) {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
} else {
@@ -1404,7 +1370,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
* @param timeMs The time in milliseconds to seek to.
*/
@Override
- public void timeshiftSeekTo(long timeMs) {
+ public void timeShiftSeekTo(long timeMs) {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
}
@@ -1413,14 +1379,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
/** Returns the current playback position in milliseconds. */
@Override
- public long timeshiftGetCurrentPositionMs() {
+ public long timeShiftGetCurrentPositionMs() {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
}
if (DEBUG) {
Log.d(
TAG,
- "timeshiftGetCurrentPositionMs: current position ="
+ "timeShiftGetCurrentPositionMs: current position ="
+ Utils.toTimeString(mTimeShiftCurrentPositionMs));
}
return mTimeShiftCurrentPositionMs;
@@ -1446,12 +1412,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
};
}
- /** Listens for dpad actions that are otherwise trapped by talkback */
- public interface OnTalkBackDpadKeyListener {
-
- void onTalkBackDpadKey(int keycode);
- }
-
/** A listener which receives the notification when the screen is blocked/unblocked. */
public abstract static class OnScreenBlockingChangedListener {
/** Called when the screen is blocked/unblocked. */
diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java
index 222fcb3a..b2854a1f 100644
--- a/src/com/android/tv/ui/TvOverlayManager.java
+++ b/src/com/android/tv/ui/TvOverlayManager.java
@@ -86,19 +86,18 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
@Retention(RetentionPolicy.SOURCE)
@IntDef(
- flag = true,
- value = {
- FLAG_HIDE_OVERLAYS_DEFAULT,
- FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION,
- FLAG_HIDE_OVERLAYS_KEEP_SCENE,
- FLAG_HIDE_OVERLAYS_KEEP_DIALOG,
- FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS,
- FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY,
- FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE,
- FLAG_HIDE_OVERLAYS_KEEP_MENU,
- FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT
- }
- )
+ flag = true,
+ value = {
+ FLAG_HIDE_OVERLAYS_DEFAULT,
+ FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION,
+ FLAG_HIDE_OVERLAYS_KEEP_SCENE,
+ FLAG_HIDE_OVERLAYS_KEEP_DIALOG,
+ FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS,
+ FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY,
+ FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE,
+ FLAG_HIDE_OVERLAYS_KEEP_MENU,
+ FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT
+ })
private @interface HideOverlayFlag {}
// FLAG_HIDE_OVERLAYs must be bitwise exclusive.
public static final int FLAG_HIDE_OVERLAYS_DEFAULT = 0b000000000;
@@ -115,20 +114,19 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
@Retention(RetentionPolicy.SOURCE)
@IntDef(
- flag = true,
- value = {
- OVERLAY_TYPE_NONE,
- OVERLAY_TYPE_MENU,
- OVERLAY_TYPE_SIDE_FRAGMENT,
- OVERLAY_TYPE_DIALOG,
- OVERLAY_TYPE_GUIDE,
- OVERLAY_TYPE_SCENE_CHANNEL_BANNER,
- OVERLAY_TYPE_SCENE_INPUT_BANNER,
- OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH,
- OVERLAY_TYPE_SCENE_SELECT_INPUT,
- OVERLAY_TYPE_FRAGMENT
- }
- )
+ flag = true,
+ value = {
+ OVERLAY_TYPE_NONE,
+ OVERLAY_TYPE_MENU,
+ OVERLAY_TYPE_SIDE_FRAGMENT,
+ OVERLAY_TYPE_DIALOG,
+ OVERLAY_TYPE_GUIDE,
+ OVERLAY_TYPE_SCENE_CHANNEL_BANNER,
+ OVERLAY_TYPE_SCENE_INPUT_BANNER,
+ OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH,
+ OVERLAY_TYPE_SCENE_SELECT_INPUT,
+ OVERLAY_TYPE_FRAGMENT
+ })
private @interface TvOverlayType {}
// OVERLAY_TYPEs must be bitwise exclusive.
/** The overlay type which indicates that there are no overlays. */
@@ -176,6 +174,8 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
public static final int UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK = 5;
/** Updates channel banner because of stream info updating. */
public static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO = 6;
+ /** Updates channel banner because of channel signal updating. */
+ public static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH = 7;
private static final String FRAGMENT_TAG_SETUP_SOURCES = "tag_setup_sources";
private static final String FRAGMENT_TAG_NEW_SOURCES = "tag_new_sources";
@@ -287,35 +287,17 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
mSideFragmentManager =
new SideFragmentManager(
mainActivity,
- new Runnable() {
- @Override
- public void run() {
- onOverlayOpened(OVERLAY_TYPE_SIDE_FRAGMENT);
- hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS);
- }
+ () -> {
+ onOverlayOpened(OVERLAY_TYPE_SIDE_FRAGMENT);
+ hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS);
},
- new Runnable() {
- @Override
- public void run() {
- showChannelBannerIfHiddenBySideFragment();
- onOverlayClosed(OVERLAY_TYPE_SIDE_FRAGMENT);
- }
+ () -> {
+ showChannelBannerIfHiddenBySideFragment();
+ onOverlayClosed(OVERLAY_TYPE_SIDE_FRAGMENT);
});
// Program Guide
- Runnable preShowRunnable =
- new Runnable() {
- @Override
- public void run() {
- onOverlayOpened(OVERLAY_TYPE_GUIDE);
- }
- };
- Runnable postHideRunnable =
- new Runnable() {
- @Override
- public void run() {
- onOverlayClosed(OVERLAY_TYPE_GUIDE);
- }
- };
+ Runnable preShowRunnable = () -> onOverlayOpened(OVERLAY_TYPE_GUIDE);
+ Runnable postHideRunnable = () -> onOverlayClosed(OVERLAY_TYPE_GUIDE);
DvrDataManager dvrDataManager =
CommonFeatures.DVR.isEnabled(mainActivity) ? singletons.getDvrDataManager() : null;
mProgramGuide =
@@ -520,16 +502,13 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
onOverlayOpened(OVERLAY_TYPE_FRAGMENT);
runAfterSideFragmentsAreClosed(
- new Runnable() {
- @Override
- public void run() {
- if (DEBUG) Log.d(TAG, "showFragment(" + fragment + ")");
- mMainActivity
- .getFragmentManager()
- .beginTransaction()
- .replace(R.id.fragment_container, fragment, tag)
- .commit();
- }
+ () -> {
+ if (DEBUG) Log.d(TAG, "showFragment(" + fragment + ")");
+ mMainActivity
+ .getFragmentManager()
+ .beginTransaction()
+ .replace(R.id.fragment_container, fragment, tag)
+ .commit();
});
}
@@ -678,12 +657,7 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
/** Shows the program guide. */
public void showProgramGuide() {
mProgramGuide.show(
- new Runnable() {
- @Override
- public void run() {
- hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE);
- }
- });
+ () -> hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE));
}
/**
@@ -855,6 +829,10 @@ public class TvOverlayManager implements AccessibilityStateChangeListener {
&& lockType != ChannelBannerView.LOCK_PROGRAM_DETAIL) {
mChannelBannerView.updateViews(false);
}
+ } else if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mMainActivity)
+ && reason == UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH) {
+ mChannelBannerView.updateChannelSignalStrengthView(
+ mTvView.getChannelSignalStrength());
} else {
mChannelBannerView.updateViews(
reason == UPDATE_CHANNEL_BANNER_REASON_TUNE
diff --git a/src/com/android/tv/ui/TvTransitionManager.java b/src/com/android/tv/ui/TvTransitionManager.java
index 5af3e6f2..f60337f1 100644
--- a/src/com/android/tv/ui/TvTransitionManager.java
+++ b/src/com/android/tv/ui/TvTransitionManager.java
@@ -174,28 +174,19 @@ public class TvTransitionManager extends TransitionManager {
mEmptyScene = new Scene(mSceneContainer, (View) mEmptyView);
mEmptyScene.setEnterAction(
- new Runnable() {
- @Override
- public void run() {
- FrameLayout.LayoutParams emptySceneLayoutParams =
- (FrameLayout.LayoutParams) mEmptyView.getLayoutParams();
- ViewGroup.MarginLayoutParams lp =
- (ViewGroup.MarginLayoutParams) mCurrentSceneView.getLayoutParams();
- emptySceneLayoutParams.topMargin = mCurrentSceneView.getTop();
- emptySceneLayoutParams.setMarginStart(lp.getMarginStart());
- emptySceneLayoutParams.height = mCurrentSceneView.getHeight();
- emptySceneLayoutParams.width = mCurrentSceneView.getWidth();
- mEmptyView.setLayoutParams(emptySceneLayoutParams);
- setCurrentScene(mEmptyScene, mEmptyView);
- }
- });
- mEmptyScene.setExitAction(
- new Runnable() {
- @Override
- public void run() {
- removeAllViewsFromOverlay();
- }
+ () -> {
+ FrameLayout.LayoutParams emptySceneLayoutParams =
+ (FrameLayout.LayoutParams) mEmptyView.getLayoutParams();
+ ViewGroup.MarginLayoutParams lp =
+ (ViewGroup.MarginLayoutParams) mCurrentSceneView.getLayoutParams();
+ emptySceneLayoutParams.topMargin = mCurrentSceneView.getTop();
+ emptySceneLayoutParams.setMarginStart(lp.getMarginStart());
+ emptySceneLayoutParams.height = mCurrentSceneView.getHeight();
+ emptySceneLayoutParams.width = mCurrentSceneView.getWidth();
+ mEmptyView.setLayoutParams(emptySceneLayoutParams);
+ setCurrentScene(mEmptyScene, mEmptyView);
});
+ mEmptyScene.setExitAction(this::removeAllViewsFromOverlay);
mChannelBannerScene = buildScene(mSceneContainer, mChannelBannerView);
mInputBannerScene = buildScene(mSceneContainer, mInputBannerView);
@@ -274,21 +265,15 @@ public class TvTransitionManager extends TransitionManager {
private Scene buildScene(ViewGroup sceneRoot, final TransitionLayout layout) {
final Scene scene = new Scene(sceneRoot, (View) layout);
scene.setEnterAction(
- new Runnable() {
- @Override
- public void run() {
- boolean wasEmptyScene = (mCurrentScene == mEmptyScene);
- setCurrentScene(scene, (ViewGroup) layout);
- layout.onEnterAction(wasEmptyScene);
- }
+ () -> {
+ boolean wasEmptyScene = (mCurrentScene == mEmptyScene);
+ setCurrentScene(scene, (ViewGroup) layout);
+ layout.onEnterAction(wasEmptyScene);
});
scene.setExitAction(
- new Runnable() {
- @Override
- public void run() {
- removeAllViewsFromOverlay();
- layout.onExitAction();
- }
+ () -> {
+ removeAllViewsFromOverlay();
+ layout.onExitAction();
});
return scene;
}
diff --git a/src/com/android/tv/ui/TvViewUiManager.java b/src/com/android/tv/ui/TvViewUiManager.java
index 7e354db3..b7e8b433 100644
--- a/src/com/android/tv/ui/TvViewUiManager.java
+++ b/src/com/android/tv/ui/TvViewUiManager.java
@@ -43,9 +43,9 @@ import android.view.ViewGroup.MarginLayoutParams;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import com.android.tv.R;
-import com.android.tv.TvFeatures;
import com.android.tv.TvOptionsManager;
import com.android.tv.data.DisplayMode;
+import com.android.tv.features.TvFeatures;
import com.android.tv.util.TvSettings;
/**
@@ -460,12 +460,7 @@ public class TvViewUiManager {
return;
}
mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- setTvViewPosition(mTvViewLayoutParams, mTvViewFrame, false);
- }
- });
+ () -> setTvViewPosition(mTvViewLayoutParams, mTvViewFrame, false));
}
});
mTvViewAnimator.addUpdateListener(
@@ -496,13 +491,7 @@ public class TvViewUiManager {
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- mContentView.setBackgroundColor(mBackgroundColor);
- }
- });
+ mHandler.post(() -> mContentView.setBackgroundColor(mBackgroundColor));
}
});
}
diff --git a/src/com/android/tv/ui/TunableTvViewPlayingApi.java b/src/com/android/tv/ui/api/TunableTvViewPlayingApi.java
index 3f19b61f..eb1f030d 100644
--- a/src/com/android/tv/ui/TunableTvViewPlayingApi.java
+++ b/src/com/android/tv/ui/api/TunableTvViewPlayingApi.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.ui;
+package com.android.tv.ui.api;
/** API to play pause and set the volume of a TunableTvView */
public interface TunableTvViewPlayingApi {
@@ -27,17 +27,17 @@ public interface TunableTvViewPlayingApi {
boolean isTimeShiftAvailable();
- void timeshiftPlay();
+ void timeShiftPlay();
- void timeshiftPause();
+ void timeShiftPause();
- void timeshiftRewind(int speed);
+ void timeShiftRewind(int speed);
- void timeshiftFastForward(int speed);
+ void timeShiftFastForward(int speed);
- void timeshiftSeekTo(long timeMs);
+ void timeShiftSeekTo(long timeMs);
- long timeshiftGetCurrentPositionMs();
+ long timeShiftGetCurrentPositionMs();
/** Used to receive the time-shift events. */
abstract class TimeShiftListener {
diff --git a/src/com/android/tv/ui/hideable/AutoHideScheduler.java b/src/com/android/tv/ui/hideable/AutoHideScheduler.java
index 75859792..8bf70de1 100644
--- a/src/com/android/tv/ui/hideable/AutoHideScheduler.java
+++ b/src/com/android/tv/ui/hideable/AutoHideScheduler.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2018 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.hideable;
import android.content.Context;
diff --git a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
index 48b80723..62130b64 100644
--- a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
+++ b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
@@ -37,7 +37,6 @@ import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
@@ -213,17 +212,14 @@ public class CustomizeChannelListFragment extends SideFragment {
ArrayList<Channel> channels = new ArrayList<>(mChannels);
Collections.sort(
channels,
- new Comparator<Channel>() {
- @Override
- public int compare(Channel lhs, Channel rhs) {
- boolean lhsHd = isHdChannel(lhs);
- boolean rhsHd = isHdChannel(rhs);
- if (lhsHd == rhsHd) {
- return ChannelNumber.compare(
- lhs.getDisplayNumber(), rhs.getDisplayNumber());
- } else {
- return lhsHd ? -1 : 1;
- }
+ (Channel lhs, Channel rhs) -> {
+ boolean lhsHd = isHdChannel(lhs);
+ boolean rhsHd = isHdChannel(rhs);
+ if (lhsHd == rhsHd) {
+ return ChannelNumber.compare(
+ lhs.getDisplayNumber(), rhs.getDisplayNumber());
+ } else {
+ return lhsHd ? -1 : 1;
}
});
diff --git a/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java b/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java
index 03b71c8c..7a65247f 100644
--- a/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java
+++ b/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java
@@ -20,7 +20,7 @@ import android.media.tv.TvTrackInfo;
import android.text.TextUtils;
import android.view.KeyEvent;
import com.android.tv.R;
-import com.android.tv.util.Utils;
+import com.android.tv.util.TvTrackInfoUtils;
import java.util.ArrayList;
import java.util.List;
@@ -51,12 +51,13 @@ public class MultiAudioFragment extends SideFragment {
List<Item> items = new ArrayList<>();
if (tracks != null) {
- boolean needToShowSampleRate = Utils.needToShowSampleRate(getActivity(), tracks);
+ boolean needToShowSampleRate = TvTrackInfoUtils
+ .needToShowSampleRate(getActivity(), tracks);
int pos = 0;
for (final TvTrackInfo track : tracks) {
RadioButtonItem item =
new MultiAudioOptionItem(
- Utils.getMultiAudioString(
+ TvTrackInfoUtils.getMultiAudioString(
getActivity(), track, needToShowSampleRate),
track.getId());
if (track.getId().equals(mSelectedTrackId)) {
diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
index 31d00fa6..aa71fb75 100644
--- a/src/com/android/tv/ui/sidepanel/SettingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
@@ -16,8 +16,6 @@
package com.android.tv.ui.sidepanel;
-import static com.android.tv.TvFeatures.TUNER;
-
import android.app.ApplicationErrorReport;
import android.content.Intent;
import android.media.tv.TvInputInfo;
@@ -81,10 +79,9 @@ public class SettingsFragment extends SideFragment {
customizeChannelListItem.setEnabled(false);
items.add(customizeChannelListItem);
final MainActivity activity = getMainActivity();
+ TvSingletons singletons = TvSingletons.getSingletons(getContext());
boolean hasNewInput =
- TvSingletons.getSingletons(getContext())
- .getSetupUtils()
- .hasNewInput(activity.getTvInputManagerHelper());
+ singletons.getSetupUtils().hasNewInput(activity.getTvInputManagerHelper());
items.add(
new ActionItem(
getString(R.string.settings_channel_source_item_setup),
@@ -127,11 +124,9 @@ public class SettingsFragment extends SideFragment {
// It's TBD.
}
boolean showTrickplaySetting = false;
- if (TUNER.isEnabled(getContext())) {
+ if (singletons.getBuiltInTunerManager().isPresent()) {
for (TvInputInfo inputInfo :
- TvSingletons.getSingletons(getContext())
- .getTvInputManagerHelper()
- .getTvInputInfos(true, true)) {
+ singletons.getTvInputManagerHelper().getTvInputInfos(true, true)) {
if (Utils.isInternalTvInput(getContext(), inputInfo.getId())) {
showTrickplaySetting = true;
break;
diff --git a/src/com/android/tv/ui/sidepanel/SideFragment.java b/src/com/android/tv/ui/sidepanel/SideFragment.java
index 2902ea7f..590f1300 100644
--- a/src/com/android/tv/ui/sidepanel/SideFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SideFragment.java
@@ -342,12 +342,9 @@ public abstract class SideFragment<T extends Item> extends Fragment implements H
}
if (view.getBackground() instanceof RippleDrawable) {
view.postDelayed(
- new Runnable() {
- @Override
- public void run() {
- if (mItem != null) {
- mItem.onSelected();
- }
+ () -> {
+ if (mItem != null) {
+ mItem.onSelected();
}
},
view.getResources().getInteger(R.integer.side_panel_ripple_anim_duration));
diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java
index 4e3cf7fb..b14bf78d 100644
--- a/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java
+++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java
@@ -41,7 +41,6 @@ import com.android.tv.ui.sidepanel.Item;
import com.android.tv.ui.sidepanel.SideFragment;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Comparator;
import java.util.List;
public class ChannelsBlockedFragment extends SideFragment {
@@ -132,15 +131,11 @@ public class ChannelsBlockedFragment extends SideFragment {
mChannels.addAll(getChannelDataManager().getChannelList());
Collections.sort(
mChannels,
- new Comparator<Channel>() {
- @Override
- public int compare(Channel lhs, Channel rhs) {
- if (lhs.isBrowsable() != rhs.isBrowsable()) {
- return lhs.isBrowsable() ? -1 : 1;
- }
- return ChannelNumber.compare(
- lhs.getDisplayNumber(), rhs.getDisplayNumber());
+ (Channel lhs, Channel rhs) -> {
+ if (lhs.isBrowsable() != rhs.isBrowsable()) {
+ return lhs.isBrowsable() ? -1 : 1;
}
+ return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber());
});
final long currentChannelId = getMainActivity().getCurrentChannelId();
diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
index 128fcd1a..d1ae4423 100644
--- a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
@@ -39,6 +39,7 @@ import com.android.tv.ui.sidepanel.RadioButtonItem;
import com.android.tv.ui.sidepanel.SideFragment;
import com.android.tv.util.TvSettings;
import com.android.tv.util.TvSettings.ContentRatingLevel;
+import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -167,7 +168,7 @@ public class RatingsFragment extends SideFragment {
super.onUpdate();
setChecked(
mParentalControlSettings.isRatingBlocked(
- new TvContentRating[] {TvContentRating.UNRATED}));
+ ImmutableList.of(TvContentRating.UNRATED)));
}
@Override
@@ -239,7 +240,7 @@ public class RatingsFragment extends SideFragment {
// set checked if UNRATED is blocked, and set unchecked otherwise.
mBlockUnratedItem.setChecked(
mParentalControlSettings.isRatingBlocked(
- new TvContentRating[] {TvContentRating.UNRATED}));
+ ImmutableList.of(TvContentRating.UNRATED)));
}
notifyItemsChanged(mRatingLevelItems.size());
}
diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java
index 60fa3018..b3523952 100644
--- a/src/com/android/tv/util/AsyncDbTask.java
+++ b/src/com/android/tv/util/AsyncDbTask.java
@@ -17,6 +17,7 @@
package com.android.tv.util;
import android.content.ContentResolver;
+import android.content.Context;
import android.database.Cursor;
import android.media.tv.TvContract;
import android.media.tv.TvContract.Programs;
@@ -34,9 +35,12 @@ import com.android.tv.data.ChannelImpl;
import com.android.tv.data.Program;
import com.android.tv.data.api.Channel;
import com.android.tv.dvr.data.RecordedProgram;
+import com.google.common.base.Predicate;
+import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
+import javax.inject.Qualifier;
/**
* {@link AsyncTask} that defaults to executing on its own single threaded Executor Service.
@@ -50,6 +54,10 @@ public abstract class AsyncDbTask<Params, Progress, Result>
private static final String TAG = "AsyncDbTask";
private static final boolean DEBUG = false;
+ /** Annotation for requesting the {@link Executor} for data base access. */
+ @Qualifier
+ public @interface DbExecutor {}
+
private final Executor mExecutor;
boolean mCalledExecuteOnDbThread;
@@ -67,23 +75,23 @@ public abstract class AsyncDbTask<Params, Progress, Result>
* @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;
+ private final WeakReference<Context> mContextReference;
private final Uri mUri;
- private final String[] mProjection;
private final String mSelection;
private final String[] mSelectionArgs;
private final String mOrderBy;
+ private String[] mProjection;
public AsyncQueryTask(
- Executor executor,
- ContentResolver contentResolver,
+ @DbExecutor Executor executor,
+ Context context,
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String orderBy) {
super(executor);
- mContentResolver = contentResolver;
+ mContextReference = new WeakReference<>(context);
mUri = uri;
mProjection = projection;
mSelection = selection;
@@ -110,12 +118,35 @@ public abstract class AsyncDbTask<Params, Progress, Result>
// This is guaranteed to never call onPostExecute because the task is canceled.
return null;
}
+ Context context = mContextReference.get();
+ if (context == null) {
+ return null;
+ }
+ if (Utils.isProgramsUri(mUri)
+ && TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) {
+ mProjection =
+ TvProviderUtils.addExtraColumnsToProjection(
+ mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
+ } else if (Utils.isRecordedProgramsUri(mUri)) {
+ if (TvProviderUtils.checkSeriesIdColumn(
+ context, TvContract.RecordedPrograms.CONTENT_URI)) {
+ mProjection =
+ TvProviderUtils.addExtraColumnsToProjection(
+ mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
+ }
+ if (TvProviderUtils.checkStateColumn(
+ context, TvContract.RecordedPrograms.CONTENT_URI)) {
+ mProjection =
+ TvProviderUtils.addExtraColumnsToProjection(
+ mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_STATE);
+ }
+ }
if (DEBUG) {
Log.v(TAG, "Starting query for " + this);
}
try (Cursor c =
- mContentResolver.query(
- mUri, mProjection, mSelection, mSelectionArgs, mOrderBy)) {
+ context.getContentResolver()
+ .query(mUri, mProjection, mSelection, mSelectionArgs, mOrderBy)) {
if (c != null && !isCancelled()) {
Result result = onQuery(c);
if (DEBUG) {
@@ -164,33 +195,25 @@ public abstract class AsyncDbTask<Params, Progress, Result>
public AsyncQueryListTask(
Executor executor,
- ContentResolver contentResolver,
+ Context context,
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String orderBy) {
- this(
- executor,
- contentResolver,
- uri,
- projection,
- selection,
- selectionArgs,
- orderBy,
- null);
+ this(executor, context, uri, projection, selection, selectionArgs, orderBy, null);
}
public AsyncQueryListTask(
Executor executor,
- ContentResolver contentResolver,
+ Context context,
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String orderBy,
CursorFilter filter) {
- super(executor, contentResolver, uri, projection, selection, selectionArgs, orderBy);
+ super(executor, context, uri, projection, selection, selectionArgs, orderBy);
mFilter = filter;
}
@@ -202,7 +225,7 @@ public abstract class AsyncDbTask<Params, Progress, Result>
// This is guaranteed to never call onPostExecute because the task is canceled.
return null;
}
- if (mFilter != null && !mFilter.filter(c)) {
+ if (mFilter != null && !mFilter.apply(c)) {
continue;
}
T t = fromCursor(c);
@@ -237,13 +260,13 @@ public abstract class AsyncDbTask<Params, Progress, Result>
public AsyncQueryItemTask(
Executor executor,
- ContentResolver contentResolver,
+ Context context,
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String orderBy) {
- super(executor, contentResolver, uri, projection, selection, selectionArgs, orderBy);
+ super(executor, context, uri, projection, selection, selectionArgs, orderBy);
}
@Override
@@ -283,10 +306,10 @@ public abstract class AsyncDbTask<Params, Progress, Result>
/** Gets an {@link List} of {@link Channel}s from {@link TvContract.Channels#CONTENT_URI}. */
public abstract static class AsyncChannelQueryTask extends AsyncQueryListTask<Channel> {
- public AsyncChannelQueryTask(Executor executor, ContentResolver contentResolver) {
+ public AsyncChannelQueryTask(Executor executor, Context context) {
super(
executor,
- contentResolver,
+ context,
TvContract.Channels.CONTENT_URI,
ChannelImpl.PROJECTION,
null,
@@ -302,20 +325,13 @@ public abstract class AsyncDbTask<Params, Progress, Result>
/** Gets an {@link List} of {@link Program}s from {@link TvContract.Programs#CONTENT_URI}. */
public abstract static class AsyncProgramQueryTask extends AsyncQueryListTask<Program> {
- public AsyncProgramQueryTask(Executor executor, ContentResolver contentResolver) {
- super(
- executor,
- contentResolver,
- Programs.CONTENT_URI,
- Program.PROJECTION,
- null,
- null,
- null);
+ public AsyncProgramQueryTask(Executor executor, Context context) {
+ super(executor, context, Programs.CONTENT_URI, Program.PROJECTION, null, null, null);
}
public AsyncProgramQueryTask(
Executor executor,
- ContentResolver contentResolver,
+ Context context,
Uri uri,
String selection,
String[] selectionArgs,
@@ -323,7 +339,7 @@ public abstract class AsyncDbTask<Params, Progress, Result>
CursorFilter filter) {
super(
executor,
- contentResolver,
+ context,
uri,
Program.PROJECTION,
selection,
@@ -341,9 +357,8 @@ public abstract class AsyncDbTask<Params, Progress, Result>
/** Gets an {@link List} of {@link TvContract.RecordedPrograms}s. */
public abstract static class AsyncRecordedProgramQueryTask
extends AsyncQueryListTask<RecordedProgram> {
- public AsyncRecordedProgramQueryTask(
- Executor executor, ContentResolver contentResolver, Uri uri) {
- super(executor, contentResolver, uri, RecordedProgram.PROJECTION, null, null, null);
+ public AsyncRecordedProgramQueryTask(Executor executor, Context context, Uri uri) {
+ super(executor, context, uri, RecordedProgram.PROJECTION, null, null, null);
}
@Override
@@ -370,13 +385,10 @@ public abstract class AsyncDbTask<Params, Progress, Result>
protected final long mChannelId;
public LoadProgramsForChannelTask(
- Executor executor,
- ContentResolver contentResolver,
- long channelId,
- @Nullable Range<Long> period) {
+ Executor executor, Context context, long channelId, @Nullable Range<Long> period) {
super(
executor,
- contentResolver,
+ context,
period == null
? TvContract.buildProgramsUriForChannel(channelId)
: TvContract.buildProgramsUriForChannel(
@@ -401,11 +413,10 @@ public abstract class AsyncDbTask<Params, Progress, Result>
/** Gets a single {@link Program} from {@link TvContract.Programs#CONTENT_URI}. */
public static class AsyncQueryProgramTask extends AsyncQueryItemTask<Program> {
- public AsyncQueryProgramTask(
- Executor executor, ContentResolver contentResolver, long programId) {
+ public AsyncQueryProgramTask(Executor executor, Context context, long programId) {
super(
executor,
- contentResolver,
+ context,
TvContract.buildProgramUri(programId),
Program.PROJECTION,
null,
@@ -420,5 +431,5 @@ public abstract class AsyncDbTask<Params, Progress, Result>
}
/** An interface which filters the row. */
- public interface CursorFilter extends Filter<Cursor> {}
+ public interface CursorFilter extends Predicate<Cursor> {}
}
diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java
index 764689c2..82e8a94a 100644
--- a/src/com/android/tv/util/RecurringRunner.java
+++ b/src/com/android/tv/util/RecurringRunner.java
@@ -99,17 +99,14 @@ public final class RecurringRunner {
long delay = Math.max(next - now, 0);
boolean posted =
mHandler.postDelayed(
- new Runnable() {
- @Override
- public void run() {
- try {
- if (DEBUG) Log.i(TAG, "Starting " + mName);
- mRunnable.run();
- } catch (Exception e) {
- Log.w(TAG, "Error running " + mName, e);
- }
- postAt(resetNextRunTime());
+ () -> {
+ try {
+ if (DEBUG) Log.i(TAG, "Starting " + mName);
+ mRunnable.run();
+ } catch (Exception e) {
+ Log.w(TAG, "Error running " + mName, e);
}
+ postAt(resetNextRunTime());
},
delay);
if (!posted) {
diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java
index 0d536320..a9b67fa8 100644
--- a/src/com/android/tv/util/SetupUtils.java
+++ b/src/com/android/tv/util/SetupUtils.java
@@ -28,20 +28,25 @@ import android.media.tv.TvInputManager;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
-import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import com.android.tv.TvSingletons;
-import com.android.tv.common.BaseApplication;
import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
+import com.android.tv.common.singletons.HasTvInputId;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.api.Channel;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
+import com.google.common.base.Optional;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
+import javax.inject.Inject;
+import javax.inject.Singleton;
/** A utility class related to input setup. */
+@Singleton
public class SetupUtils {
private static final String TAG = "SetupUtils";
private static final boolean DEBUG = false;
@@ -61,10 +66,12 @@ public class SetupUtils {
private final Set<String> mSetUpInputs;
private final Set<String> mRecognizedInputs;
private boolean mIsFirstTune;
- private final String mTunerInputId;
+ private final Optional<String> mOptionalTunerInputId;
- @VisibleForTesting
- protected SetupUtils(Context context) {
+ @Inject
+ public SetupUtils(
+ @ApplicationContext Context context,
+ Optional<BuiltInTunerManager> optionalBuiltInTunerManager) {
mContext = context;
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
mSetUpInputs = new ArraySet<>();
@@ -77,16 +84,8 @@ public class SetupUtils {
mRecognizedInputs.addAll(
mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs));
mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true);
- mTunerInputId = BaseApplication.getSingletons(context).getEmbeddedTunerInputId();
- }
-
- /**
- * Creates an instance of {@link SetupUtils}.
- *
- * <p><b>WARNING</b> this should only be called by the top level application.
- */
- public static SetupUtils createForTvSingletons(Context context) {
- return new SetupUtils(context.getApplicationContext());
+ mOptionalTunerInputId =
+ optionalBuiltInTunerManager.transform(HasTvInputId::getEmbeddedTunerInputId);
}
/** Additional work after the setup of TV input. */
@@ -124,32 +123,29 @@ public class SetupUtils {
TvSingletons tvSingletons = TvSingletons.getSingletons(context);
final ChannelDataManager manager = tvSingletons.getChannelDataManager();
manager.updateChannels(
- new Runnable() {
- @Override
- public void run() {
- Channel firstChannelForInput = null;
- boolean browsableChanged = false;
- for (Channel channel : manager.getChannelList()) {
- if (channel.getInputId().equals(inputId)) {
- if (!channel.isBrowsable()) {
- manager.updateBrowsable(channel.getId(), true, true);
- browsableChanged = true;
- }
- if (firstChannelForInput == null) {
- firstChannelForInput = channel;
- }
+ () -> {
+ Channel firstChannelForInput = null;
+ boolean browsableChanged = false;
+ for (Channel channel : manager.getChannelList()) {
+ if (channel.getInputId().equals(inputId)) {
+ if (!channel.isBrowsable()) {
+ manager.updateBrowsable(channel.getId(), true, true);
+ browsableChanged = true;
+ }
+ if (firstChannelForInput == null) {
+ firstChannelForInput = channel;
}
}
- if (firstChannelForInput != null) {
- Utils.setLastWatchedChannel(context, firstChannelForInput);
- }
- if (browsableChanged) {
- manager.notifyChannelBrowsableChanged();
- manager.applyUpdatedValuesToDb();
- }
- if (postRunnable != null) {
- postRunnable.run();
- }
+ }
+ if (firstChannelForInput != null) {
+ Utils.setLastWatchedChannel(context, firstChannelForInput);
+ }
+ if (browsableChanged) {
+ manager.notifyChannelBrowsableChanged();
+ manager.applyUpdatedValuesToDb();
+ }
+ if (postRunnable != null) {
+ postRunnable.run();
}
});
}
@@ -332,7 +328,9 @@ public class SetupUtils {
// A USB tuner device can be temporarily unplugged. We do not remove the USB tuner input
// from the known inputs so that the input won't appear as a new input whenever the user
// plugs in the USB tuner device again.
- removedInputList.remove(mTunerInputId);
+ if (mOptionalTunerInputId.isPresent()) {
+ removedInputList.remove(mOptionalTunerInputId.get());
+ }
if (!removedInputList.isEmpty()) {
boolean inputPackageDeleted = false;
diff --git a/src/com/android/tv/util/SqlParams.java b/src/com/android/tv/util/SqlParams.java
index c4b803b6..fa557ba2 100644
--- a/src/com/android/tv/util/SqlParams.java
+++ b/src/com/android/tv/util/SqlParams.java
@@ -17,15 +17,16 @@
package com.android.tv.util;
import android.database.DatabaseUtils;
+import android.support.annotation.Nullable;
import java.util.Arrays;
/** Convenience class for SQL operations. */
public class SqlParams {
private String mTables;
- private String mSelection;
- private String[] mSelectionArgs;
+ private @Nullable String mSelection;
+ private @Nullable String[] mSelectionArgs;
- public SqlParams(String tables, String selection, String... selectionArgs) {
+ public SqlParams(String tables, @Nullable String selection, @Nullable String... selectionArgs) {
setTables(tables);
setWhere(selection, selectionArgs);
}
@@ -34,11 +35,11 @@ public class SqlParams {
return mTables;
}
- public String getSelection() {
+ public @Nullable String getSelection() {
return mSelection;
}
- public String[] getSelectionArgs() {
+ public @Nullable String[] getSelectionArgs() {
return mSelectionArgs;
}
diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java
index 625fb7b2..cb7d9854 100644
--- a/src/com/android/tv/util/TvInputManagerHelper.java
+++ b/src/com/android/tv/util/TvInputManagerHelper.java
@@ -19,21 +19,29 @@ package com.android.tv.util;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.database.ContentObserver;
import android.graphics.drawable.Drawable;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.media.tv.TvContentRatingSystemInfo;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputManager.TvInputCallback;
+import android.net.Uri;
import android.os.Handler;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
-import com.android.tv.TvFeatures;
import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.compat.TvInputInfoCompat;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.util.SystemProperties;
+import com.android.tv.features.TvFeatures;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.util.images.ImageCache;
@@ -46,7 +54,12 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+/** Helper class for {@link TvInputManager}. */
+@UiThread
+@Singleton
public class TvInputManagerHelper {
private static final String TAG = "TvInputManagerHelper";
private static final boolean DEBUG = false;
@@ -117,6 +130,12 @@ public class TvInputManagerHelper {
};
private static final String META_LABEL_SORT_KEY = "input_sort_key";
+ private static final String TV_INPUT_ALLOW_3RD_PARTY_INPUTS = "tv_input_allow_3rd_party_inputs";
+
+ private static final String[] SYSTEM_INPUT_ID_BLACKLIST = {
+ "com.google.android.videos/" // Play Movies
+ };
+
/** The default tv input priority to show. */
private static final ArrayList<Integer> DEFAULT_TV_INPUT_PRIORITY = new ArrayList<>();
@@ -149,21 +168,24 @@ public class TvInputManagerHelper {
private final PackageManager mPackageManager;
protected final TvInputManagerInterface mTvInputManager;
private final Map<String, Integer> mInputStateMap = new HashMap<>();
- private final Map<String, TvInputInfo> mInputMap = new HashMap<>();
+ private final Map<String, TvInputInfoCompat> mInputMap = new HashMap<>();
private final Map<String, String> mTvInputLabels = new ArrayMap<>();
private final Map<String, String> mTvInputCustomLabels = new ArrayMap<>();
private final Map<String, Boolean> mInputIdToPartnerInputMap = new HashMap<>();
private final Map<String, CharSequence> mTvInputApplicationLabels = new ArrayMap<>();
private final Map<String, Drawable> mTvInputApplicationIcons = new ArrayMap<>();
- private final Map<String, Drawable> mTvInputAppliactionBanners = new ArrayMap<>();
+ private final Map<String, Drawable> mTvInputApplicationBanners = new ArrayMap<>();
+
+ private final ContentObserver mContentObserver;
private final TvInputCallback mInternalCallback =
new TvInputCallback() {
@Override
public void onInputStateChanged(String inputId, int state) {
if (DEBUG) Log.d(TAG, "onInputStateChanged " + inputId + " state=" + state);
- if (isInBlackList(inputId)) {
+ TvInputInfo info = mInputMap.get(inputId).getTvInputInfo();
+ if (info == null || isInputBlocked(info)) {
return;
}
mInputStateMap.put(inputId, state);
@@ -175,12 +197,12 @@ public class TvInputManagerHelper {
@Override
public void onInputAdded(String inputId) {
if (DEBUG) Log.d(TAG, "onInputAdded " + inputId);
- if (isInBlackList(inputId)) {
+ TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
+ if (info == null || isInputBlocked(info)) {
return;
}
- TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
if (info != null) {
- mInputMap.put(inputId, info);
+ mInputMap.put(inputId, new TvInputInfoCompat(mContext, info));
CharSequence label = info.loadLabel(mContext);
// in tests the label may be missing just use the input id
mTvInputLabels.put(inputId, label != null ? label.toString() : inputId);
@@ -205,7 +227,7 @@ public class TvInputManagerHelper {
mTvInputCustomLabels.remove(inputId);
mTvInputApplicationLabels.remove(inputId);
mTvInputApplicationIcons.remove(inputId);
- mTvInputAppliactionBanners.remove(inputId);
+ mTvInputApplicationBanners.remove(inputId);
mInputStateMap.remove(inputId);
mInputIdToPartnerInputMap.remove(inputId);
mContentRatingsManager.update();
@@ -219,11 +241,11 @@ public class TvInputManagerHelper {
@Override
public void onInputUpdated(String inputId) {
if (DEBUG) Log.d(TAG, "onInputUpdated " + inputId);
- if (isInBlackList(inputId)) {
+ TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
+ if (info == null || isInputBlocked(info)) {
return;
}
- TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
- mInputMap.put(inputId, info);
+ mInputMap.put(inputId, new TvInputInfoCompat(mContext, info));
mTvInputLabels.put(inputId, info.loadLabel(mContext).toString());
CharSequence inputCustomLabel = info.loadCustomLabel(mContext);
if (inputCustomLabel != null) {
@@ -231,7 +253,7 @@ public class TvInputManagerHelper {
}
mTvInputApplicationLabels.remove(inputId);
mTvInputApplicationIcons.remove(inputId);
- mTvInputAppliactionBanners.remove(inputId);
+ mTvInputApplicationBanners.remove(inputId);
for (TvInputCallback callback : mCallbacks) {
callback.onInputUpdated(inputId);
}
@@ -242,7 +264,10 @@ public class TvInputManagerHelper {
@Override
public void onTvInputInfoUpdated(TvInputInfo inputInfo) {
if (DEBUG) Log.d(TAG, "onTvInputInfoUpdated " + inputInfo);
- mInputMap.put(inputInfo.getId(), inputInfo);
+ if (isInputBlocked(inputInfo)) {
+ return;
+ }
+ mInputMap.put(inputInfo.getId(), new TvInputInfoCompat(mContext, inputInfo));
mTvInputLabels.put(inputInfo.getId(), inputInfo.loadLabel(mContext).toString());
CharSequence inputCustomLabel = inputInfo.loadCustomLabel(mContext);
if (inputCustomLabel != null) {
@@ -264,8 +289,10 @@ public class TvInputManagerHelper {
private final ContentRatingsManager mContentRatingsManager;
private final ParentalControlSettings mParentalControlSettings;
private final Comparator<TvInputInfo> mTvInputInfoComparator;
+ private boolean mAllow3rdPartyInputs;
- public TvInputManagerHelper(Context context) {
+ @Inject
+ public TvInputManagerHelper(@ApplicationContext Context context) {
this(context, createTvInputManagerWrapper(context));
}
@@ -285,6 +312,22 @@ public class TvInputManagerHelper {
mContentRatingsManager = new ContentRatingsManager(context, tvInputManager);
mParentalControlSettings = new ParentalControlSettings(context);
mTvInputInfoComparator = new InputComparatorInternal(this);
+ mContentObserver =
+ new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ String option = uri.getLastPathSegment();
+ if (option == null || !option.equals(TV_INPUT_ALLOW_3RD_PARTY_INPUTS)) {
+ return;
+ }
+ boolean previousSetting = mAllow3rdPartyInputs;
+ updateAllow3rdPartyInputs();
+ if (previousSetting == mAllow3rdPartyInputs) {
+ return;
+ }
+ initInputMaps();
+ }
+ };
}
public void start() {
@@ -297,30 +340,14 @@ public class TvInputManagerHelper {
}
if (DEBUG) Log.d(TAG, "start");
mStarted = true;
+ mContext.getContentResolver()
+ .registerContentObserver(
+ Settings.Global.getUriFor(TV_INPUT_ALLOW_3RD_PARTY_INPUTS),
+ true,
+ mContentObserver);
+ updateAllow3rdPartyInputs();
mTvInputManager.registerCallback(mInternalCallback, mHandler);
- mInputMap.clear();
- mTvInputLabels.clear();
- mTvInputCustomLabels.clear();
- mTvInputApplicationLabels.clear();
- mTvInputApplicationIcons.clear();
- mTvInputAppliactionBanners.clear();
- mInputStateMap.clear();
- mInputIdToPartnerInputMap.clear();
- for (TvInputInfo input : mTvInputManager.getTvInputList()) {
- if (DEBUG) Log.d(TAG, "Input detected " + input);
- String inputId = input.getId();
- if (isInBlackList(inputId)) {
- continue;
- }
- mInputMap.put(inputId, input);
- int state = mTvInputManager.getInputState(inputId);
- mInputStateMap.put(inputId, state);
- mInputIdToPartnerInputMap.put(inputId, isPartnerInput(input));
- }
- SoftPreconditions.checkState(
- mInputStateMap.size() == mInputMap.size(),
- TAG,
- "mInputStateMap not the same size as mInputMap");
+ initInputMaps();
mContentRatingsManager.update();
}
@@ -329,6 +356,7 @@ public class TvInputManagerHelper {
return;
}
mTvInputManager.unregisterCallback(mInternalCallback);
+ mContext.getContentResolver().unregisterContentObserver(mContentObserver);
mStarted = false;
mInputStateMap.clear();
mInputMap.clear();
@@ -336,8 +364,7 @@ public class TvInputManagerHelper {
mTvInputCustomLabels.clear();
mTvInputApplicationLabels.clear();
mTvInputApplicationIcons.clear();
- mTvInputAppliactionBanners.clear();
- ;
+ mTvInputApplicationBanners.clear();
mInputIdToPartnerInputMap.clear();
}
@@ -355,6 +382,9 @@ public class TvInputManagerHelper {
continue;
}
TvInputInfo input = getTvInputInfo(pair.getKey());
+ if (input == null || isInputBlocked(input)) {
+ continue;
+ }
if (tunerOnly && input.getType() != TvInputInfo.TYPE_TUNER) {
continue;
}
@@ -460,12 +490,12 @@ public class TvInputManagerHelper {
/** Gets the tv input application's banner. */
public Drawable getTvInputApplicationBanner(String inputId) {
- return mTvInputAppliactionBanners.get(inputId);
+ return mTvInputApplicationBanners.get(inputId);
}
/** Stores the tv input application's banner. */
public void setTvInputApplicationBanner(String inputId, Drawable banner) {
- mTvInputAppliactionBanners.put(inputId, banner);
+ mTvInputApplicationBanners.put(inputId, banner);
}
/** Returns if TV input exists with the input id. */
@@ -475,7 +505,14 @@ public class TvInputManagerHelper {
return mStarted && !TextUtils.isEmpty(inputId) && mInputMap.get(inputId) != null;
}
+ @Nullable
public TvInputInfo getTvInputInfo(String inputId) {
+ TvInputInfoCompat inputInfo = getTvInputInfoCompat(inputId);
+ return inputInfo == null ? null : inputInfo.getTvInputInfo();
+ }
+
+ @Nullable
+ public TvInputInfoCompat getTvInputInfoCompat(String inputId) {
SoftPreconditions.checkState(
mStarted, TAG, "getTvInputInfo() called before TvInputManagerHelper was started.");
if (!mStarted) {
@@ -494,7 +531,7 @@ public class TvInputManagerHelper {
public int getTunerTvInputSize() {
int size = 0;
- for (TvInputInfo input : mInputMap.values()) {
+ for (TvInputInfoCompat input : mInputMap.values()) {
if (input.getType() == TvInputInfo.TYPE_TUNER) {
++size;
}
@@ -601,6 +638,61 @@ public class TvInputManagerHelper {
return false;
}
+ private void initInputMaps() {
+ mInputMap.clear();
+ mTvInputLabels.clear();
+ mTvInputCustomLabels.clear();
+ mTvInputApplicationLabels.clear();
+ mTvInputApplicationIcons.clear();
+ mTvInputApplicationBanners.clear();
+ mInputStateMap.clear();
+ mInputIdToPartnerInputMap.clear();
+ for (TvInputInfo input : mTvInputManager.getTvInputList()) {
+ if (DEBUG) {
+ Log.d(TAG, "Input detected " + input);
+ }
+ String inputId = input.getId();
+ if (isInputBlocked(input)) {
+ continue;
+ }
+ mInputMap.put(inputId, new TvInputInfoCompat(mContext, input));
+ int state = mTvInputManager.getInputState(inputId);
+ mInputStateMap.put(inputId, state);
+ mInputIdToPartnerInputMap.put(inputId, isPartnerInput(input));
+ }
+ SoftPreconditions.checkState(
+ mInputStateMap.size() == mInputMap.size(),
+ TAG,
+ "mInputStateMap not the same size as mInputMap");
+ }
+
+ private void updateAllow3rdPartyInputs() {
+ int setting;
+ try {
+ setting =
+ Settings.Global.getInt(
+ mContext.getContentResolver(), TV_INPUT_ALLOW_3RD_PARTY_INPUTS);
+ } catch (SettingNotFoundException e) {
+ mAllow3rdPartyInputs = SystemProperties.ALLOW_THIRD_PARTY_INPUTS.getValue();
+ return;
+ }
+ mAllow3rdPartyInputs = setting == 1;
+ }
+
+ private boolean isInputBlocked(TvInputInfo info) {
+ if (!mAllow3rdPartyInputs) {
+ if (!isSystemInput(info)) {
+ return true;
+ }
+ for (String id : SYSTEM_INPUT_ID_BLACKLIST) {
+ if (info.getId().startsWith(id)) {
+ return true;
+ }
+ }
+ }
+ return isInBlackList(info.getId());
+ }
+
/**
* Default comparator for TvInputInfo.
*
diff --git a/src/com/android/tv/util/TvProviderUtils.java b/src/com/android/tv/util/TvProviderUtils.java
new file mode 100644
index 00000000..6b5aaecc
--- /dev/null
+++ b/src/com/android/tv/util/TvProviderUtils.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2018 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 static java.lang.Boolean.TRUE;
+
+import android.content.Context;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.StringDef;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import android.util.Log;
+import com.android.tv.data.BaseProgram;
+import com.android.tv.features.PartnerFeatures;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** A utility class related to TvProvider. */
+public final class TvProviderUtils {
+ private static final String TAG = "TvProviderUtils";
+
+ public static final String EXTRA_PROGRAM_COLUMN_SERIES_ID = BaseProgram.COLUMN_SERIES_ID;
+ public static final String EXTRA_PROGRAM_COLUMN_STATE = BaseProgram.COLUMN_STATE;
+
+ /** Possible extra columns in TV provider. */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef({EXTRA_PROGRAM_COLUMN_SERIES_ID, EXTRA_PROGRAM_COLUMN_STATE})
+ public @interface TvProviderExtraColumn {}
+
+ private static boolean sProgramHasSeriesIdColumn;
+ private static boolean sRecordedProgramHasSeriesIdColumn;
+ private static boolean sRecordedProgramHasStateColumn;
+
+ /**
+ * Checks whether a table contains a series ID column.
+ *
+ * <p>This method is different from {@link #getProgramHasSeriesIdColumn()} and {@link
+ * #getRecordedProgramHasSeriesIdColumn()} because it may access to database, so it should be
+ * run in worker thread.
+ *
+ * @return {@code true} if the corresponding table contains a series ID column; {@code false}
+ * otherwise.
+ */
+ @WorkerThread
+ public static synchronized boolean checkSeriesIdColumn(Context context, Uri uri) {
+ boolean canCreateColumn = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O);
+ if (!canCreateColumn) {
+ return false;
+ }
+ return (Utils.isRecordedProgramsUri(uri)
+ && checkRecordedProgramTableSeriesIdColumn(context, uri))
+ || (Utils.isProgramsUri(uri) && checkProgramTableSeriesIdColumn(context, uri));
+ }
+
+ @WorkerThread
+ private static synchronized boolean checkProgramTableSeriesIdColumn(Context context, Uri uri) {
+ if (!sProgramHasSeriesIdColumn) {
+ if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_SERIES_ID)) {
+ sProgramHasSeriesIdColumn = true;
+ } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_SERIES_ID)) {
+ sProgramHasSeriesIdColumn = true;
+ }
+ }
+ return sProgramHasSeriesIdColumn;
+ }
+
+ @WorkerThread
+ private static synchronized boolean checkRecordedProgramTableSeriesIdColumn(
+ Context context, Uri uri) {
+ if (!sRecordedProgramHasSeriesIdColumn) {
+ if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_SERIES_ID)) {
+ sRecordedProgramHasSeriesIdColumn = true;
+ } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_SERIES_ID)) {
+ sRecordedProgramHasSeriesIdColumn = true;
+ }
+ }
+ return sRecordedProgramHasSeriesIdColumn;
+ }
+
+ /**
+ * Checks whether a table contains a state column.
+ *
+ * <p>This method is different from {@link #getRecordedProgramHasStateColumn()} because it may
+ * access to database, so it should be run in worker thread.
+ *
+ * @return {@code true} if the corresponding table contains a state column; {@code false}
+ * otherwise.
+ */
+ @WorkerThread
+ public static synchronized boolean checkStateColumn(Context context, Uri uri) {
+ boolean canCreateColumn = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O);
+ if (!canCreateColumn) {
+ return false;
+ }
+ return (Utils.isRecordedProgramsUri(uri)
+ && checkRecordedProgramTableStateColumn(context, uri));
+ }
+
+ @WorkerThread
+ private static synchronized boolean checkRecordedProgramTableStateColumn(
+ Context context, Uri uri) {
+ if (!sRecordedProgramHasStateColumn) {
+ if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_STATE)) {
+ sRecordedProgramHasStateColumn = true;
+ } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_STATE)) {
+ sRecordedProgramHasStateColumn = true;
+ }
+ }
+ return sRecordedProgramHasStateColumn;
+ }
+
+ public static synchronized boolean getProgramHasSeriesIdColumn() {
+ return TRUE.equals(sProgramHasSeriesIdColumn);
+ }
+
+ public static synchronized boolean getRecordedProgramHasSeriesIdColumn() {
+ return TRUE.equals(sRecordedProgramHasSeriesIdColumn);
+ }
+
+ public static synchronized boolean getRecordedProgramHasStateColumn() {
+ return TRUE.equals(sRecordedProgramHasStateColumn);
+ }
+
+ public static String[] addExtraColumnsToProjection(String[] projection,
+ @TvProviderExtraColumn String column) {
+ List<String> projectionList = new ArrayList<>(Arrays.asList(projection));
+ if (!projectionList.contains(column)) {
+ projectionList.add(column);
+ }
+ projection = projectionList.toArray(projection);
+ return projection;
+ }
+
+ /**
+ * Gets column names of a table
+ *
+ * @param uri the corresponding URI of the table
+ */
+ @VisibleForTesting
+ static Set<String> getExistingColumns(Context context, Uri uri) {
+ Bundle result = null;
+ try {
+ result =
+ context.getContentResolver()
+ .call(uri, TvContract.METHOD_GET_COLUMNS, uri.toString(), null);
+ } catch (Exception e) {
+ Log.e(TAG, "Error trying to get existing columns.", e);
+ }
+ if (result != null) {
+ String[] columns = result.getStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES);
+ if (columns != null) {
+ return new HashSet<>(Arrays.asList(columns));
+ }
+ }
+ Log.e(TAG, "Query existing column names from " + uri + " returned null");
+ return Collections.emptySet();
+ }
+
+ /**
+ * Add a column to the table
+ *
+ * @return {@code true} if the column is added successfully; {@code false} otherwise.
+ */
+ private static boolean addColumnToTable(Context context, Uri contentUri, String columnName) {
+ Bundle extra = new Bundle();
+ extra.putCharSequence(TvContract.EXTRA_COLUMN_NAME, columnName);
+ extra.putCharSequence(TvContract.EXTRA_DATA_TYPE, "TEXT");
+ // If the add operation fails, the following just returns null without crashing.
+ Bundle allColumns = null;
+ try {
+ allColumns =
+ context.getContentResolver()
+ .call(
+ contentUri,
+ TvContract.METHOD_ADD_COLUMN,
+ contentUri.toString(),
+ extra);
+ } catch (Exception e) {
+ Log.e(TAG, "Error trying to add column.", e);
+ }
+ if (allColumns == null) {
+ Log.w(TAG, "Adding new column failed. Uri=" + contentUri);
+ }
+ return allColumns != null;
+ }
+
+ private TvProviderUtils() {}
+}
diff --git a/src/com/android/tv/util/TvTrackInfoUtils.java b/src/com/android/tv/util/TvTrackInfoUtils.java
index 09874502..4ec96c62 100644
--- a/src/com/android/tv/util/TvTrackInfoUtils.java
+++ b/src/com/android/tv/util/TvTrackInfoUtils.java
@@ -15,13 +15,28 @@
*/
package com.android.tv.util;
+import android.content.Context;
import android.media.tv.TvTrackInfo;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.tv.R;
import java.util.Comparator;
+import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
/** Static utilities for {@link TvTrackInfo}. */
public class TvTrackInfoUtils {
+ private static final String TAG = "TvTrackInfoUtils";
+ private static final int AUDIO_CHANNEL_NONE = 0;
+ private static final int AUDIO_CHANNEL_MONO = 1;
+ private static final int AUDIO_CHANNEL_STEREO = 2;
+ private static final int AUDIO_CHANNEL_SURROUND_6 = 6;
+ private static final int AUDIO_CHANNEL_SURROUND_8 = 8;
+
/**
* Compares how closely two {@link android.media.tv.TvTrackInfo}s match {@code language}, {@code
* channelCount} and {@code id} in that precedence.
@@ -34,40 +49,36 @@ public class TvTrackInfoUtils {
*/
public static Comparator<TvTrackInfo> createComparator(
final String id, final String language, final int channelCount) {
- return new Comparator<TvTrackInfo>() {
-
- @Override
- public int compare(TvTrackInfo lhs, TvTrackInfo rhs) {
- if (lhs == rhs) {
- return 0;
- }
- if (lhs == null) {
- return -1;
- }
- if (rhs == null) {
- return 1;
- }
- // Assumes {@code null} language matches to any language since it means user hasn't
- // selected any track before or selected a track without language information.
- boolean lhsLangMatch =
- language == null || Utils.isEqualLanguage(lhs.getLanguage(), language);
- boolean rhsLangMatch =
- language == null || Utils.isEqualLanguage(rhs.getLanguage(), language);
- if (lhsLangMatch && rhsLangMatch) {
- boolean lhsCountMatch =
- lhs.getType() != TvTrackInfo.TYPE_AUDIO
- || lhs.getAudioChannelCount() == channelCount;
- boolean rhsCountMatch =
- rhs.getType() != TvTrackInfo.TYPE_AUDIO
- || rhs.getAudioChannelCount() == channelCount;
- if (lhsCountMatch && rhsCountMatch) {
- return Boolean.compare(lhs.getId().equals(id), rhs.getId().equals(id));
- } else {
- return Boolean.compare(lhsCountMatch, rhsCountMatch);
- }
+ return (TvTrackInfo lhs, TvTrackInfo rhs) -> {
+ if (Objects.equals(lhs, rhs)) {
+ return 0;
+ }
+ if (lhs == null) {
+ return -1;
+ }
+ if (rhs == null) {
+ return 1;
+ }
+ // Assumes {@code null} language matches to any language since it means user hasn't
+ // selected any track before or selected a track without language information.
+ boolean lhsLangMatch =
+ language == null || Utils.isEqualLanguage(lhs.getLanguage(), language);
+ boolean rhsLangMatch =
+ language == null || Utils.isEqualLanguage(rhs.getLanguage(), language);
+ if (lhsLangMatch && rhsLangMatch) {
+ boolean lhsCountMatch =
+ lhs.getType() != TvTrackInfo.TYPE_AUDIO
+ || lhs.getAudioChannelCount() == channelCount;
+ boolean rhsCountMatch =
+ rhs.getType() != TvTrackInfo.TYPE_AUDIO
+ || rhs.getAudioChannelCount() == channelCount;
+ if (lhsCountMatch && rhsCountMatch) {
+ return Boolean.compare(lhs.getId().equals(id), rhs.getId().equals(id));
} else {
- return Boolean.compare(lhsLangMatch, rhsLangMatch);
+ return Boolean.compare(lhsCountMatch, rhsCountMatch);
}
+ } else {
+ return Boolean.compare(lhsLangMatch, rhsLangMatch);
}
};
}
@@ -96,5 +107,132 @@ public class TvTrackInfoUtils {
return best;
}
+ public static boolean needToShowSampleRate(Context context, List<TvTrackInfo> tracks) {
+ Set<String> multiAudioStrings = new HashSet<>();
+ for (TvTrackInfo track : tracks) {
+ String multiAudioString = getMultiAudioString(context, track, false);
+ if (multiAudioStrings.contains(multiAudioString)) {
+ return true;
+ }
+ multiAudioStrings.add(multiAudioString);
+ }
+ return false;
+ }
+
+ public static String getMultiAudioString(
+ Context context, TvTrackInfo track, boolean showSampleRate) {
+ if (track.getType() != TvTrackInfo.TYPE_AUDIO) {
+ throw new IllegalArgumentException("Not an audio track: " + toString(track));
+ }
+ String language = context.getString(R.string.multi_audio_unknown_language);
+ if (!TextUtils.isEmpty(track.getLanguage())) {
+ language = new Locale(track.getLanguage()).getDisplayName();
+ } else {
+ Log.d(
+ TAG,
+ "No language information found for the audio track: "
+ + toString(track)
+ );
+ }
+
+ StringBuilder metadata = new StringBuilder();
+ switch (track.getAudioChannelCount()) {
+ case AUDIO_CHANNEL_NONE:
+ break;
+ case AUDIO_CHANNEL_MONO:
+ metadata.append(context.getString(R.string.multi_audio_channel_mono));
+ break;
+ case AUDIO_CHANNEL_STEREO:
+ metadata.append(context.getString(R.string.multi_audio_channel_stereo));
+ break;
+ case AUDIO_CHANNEL_SURROUND_6:
+ metadata.append(context.getString(R.string.multi_audio_channel_surround_6));
+ break;
+ case AUDIO_CHANNEL_SURROUND_8:
+ metadata.append(context.getString(R.string.multi_audio_channel_surround_8));
+ break;
+ default:
+ if (track.getAudioChannelCount() > 0) {
+ metadata.append(
+ context.getString(
+ R.string.multi_audio_channel_suffix,
+ track.getAudioChannelCount()));
+ } else {
+ Log.d(
+ TAG,
+ "Invalid audio channel count ("
+ + track.getAudioChannelCount()
+ + ") found for the audio track: "
+ + toString(track));
+ }
+ break;
+ }
+ if (showSampleRate) {
+ int sampleRate = track.getAudioSampleRate();
+ if (sampleRate > 0) {
+ if (metadata.length() > 0) {
+ metadata.append(", ");
+ }
+ int integerPart = sampleRate / 1000;
+ int tenths = (sampleRate % 1000) / 100;
+ metadata.append(integerPart);
+ if (tenths != 0) {
+ metadata.append(".");
+ metadata.append(tenths);
+ }
+ metadata.append("kHz");
+ }
+ }
+
+ if (metadata.length() == 0) {
+ return language;
+ }
+ return context.getString(
+ R.string.multi_audio_display_string_with_channel, language, metadata.toString());
+ }
+
+ private static String trackTypeToString(int trackType) {
+ switch (trackType) {
+ case TvTrackInfo.TYPE_AUDIO:
+ return "Audio";
+ case TvTrackInfo.TYPE_VIDEO:
+ return "Video";
+ case TvTrackInfo.TYPE_SUBTITLE:
+ return "Subtitle";
+ default:
+ return "Invalid Type";
+ }
+ }
+
+ public static String toString(TvTrackInfo info) {
+ int trackType = info.getType();
+ return "TvTrackInfo{"
+ + "type="
+ + trackTypeToString(trackType)
+ + ", id="
+ + info.getId()
+ + ", language="
+ + info.getLanguage()
+ + ", description="
+ + info.getDescription()
+ + (trackType == TvTrackInfo.TYPE_AUDIO
+ ?
+ (", audioChannelCount="
+ + info.getAudioChannelCount()
+ + ", audioSampleRate="
+ + info.getAudioSampleRate()) : "")
+ + (trackType == TvTrackInfo.TYPE_VIDEO
+ ?
+ (", videoWidth="
+ + info.getVideoWidth()
+ + ", videoHeight="
+ + info.getVideoHeight()
+ + ", videoFrameRate="
+ + info.getVideoFrameRate()
+ + ", videoPixelAspectRatio="
+ + info.getVideoPixelAspectRatio()) : "")
+ + "}";
+ }
+
private TvTrackInfoUtils() {}
}
diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java
index a75bd446..51173739 100644
--- a/src/com/android/tv/util/Utils.java
+++ b/src/com/android/tv/util/Utils.java
@@ -29,7 +29,6 @@ import android.media.tv.TvContract;
import android.media.tv.TvContract.Channels;
import android.media.tv.TvContract.Programs.Genres;
import android.media.tv.TvInputInfo;
-import android.media.tv.TvTrackInfo;
import android.net.Uri;
import android.os.Looper;
import android.preference.PreferenceManager;
@@ -42,6 +41,7 @@ import android.util.Log;
import android.view.View;
import com.android.tv.R;
import com.android.tv.TvSingletons;
+import com.android.tv.common.BaseSingletons;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.util.Clock;
import com.android.tv.data.GenreItems;
@@ -99,12 +99,6 @@ public class Utils {
private static final int VIDEO_ULTRA_HD_WIDTH = 2048;
private static final int VIDEO_ULTRA_HD_HEIGHT = 1536;
- private static final int AUDIO_CHANNEL_NONE = 0;
- private static final int AUDIO_CHANNEL_MONO = 1;
- private static final int AUDIO_CHANNEL_STEREO = 2;
- private static final int AUDIO_CHANNEL_SURROUND_6 = 6;
- private static final int AUDIO_CHANNEL_SURROUND_8 = 8;
-
private static final long RECORDING_FAILED_REASON_NONE = 0;
private static final long HALF_MINUTE_MS = TimeUnit.SECONDS.toMillis(30);
private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1);
@@ -141,6 +135,7 @@ public class Utils {
return sb.toString();
}
+ @Nullable
@WorkerThread
public static String getInputIdForChannel(Context context, long channelId) {
if (channelId == Channel.INVALID_ID) {
@@ -153,6 +148,8 @@ public class Utils {
if (cursor != null && cursor.moveToNext()) {
return Utils.intern(cursor.getString(0));
}
+ } catch (Exception e) {
+ Log.e(TAG, "Error get input id for channel", e);
}
return null;
}
@@ -325,8 +322,17 @@ public class Utils {
Uri uri =
TvContract.buildProgramsUriForChannel(
TvContract.buildChannelUri(channelId), timeMs, timeMs);
- try (Cursor cursor =
- context.getContentResolver().query(uri, Program.PROJECTION, null, null, null)) {
+ ContentResolver resolver = context.getContentResolver();
+
+ String[] projection = Program.PROJECTION;
+ if (TvProviderUtils.checkSeriesIdColumn(context, TvContract.Programs.CONTENT_URI)) {
+ if (Utils.isProgramsUri(uri)) {
+ projection =
+ TvProviderUtils.addExtraColumnsToProjection(
+ projection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
+ }
+ }
+ try (Cursor cursor = resolver.query(uri, projection, null, null, null)) {
if (cursor != null && cursor.moveToNext()) {
return Program.fromCursor(cursor);
}
@@ -360,11 +366,10 @@ public class Utils {
Context context, long startUtcMillis, long endUtcMillis, boolean useShortFormat) {
return getDurationString(
context,
- System.currentTimeMillis(),
+ ((BaseSingletons) context.getApplicationContext()).getClock(),
startUtcMillis,
endUtcMillis,
- useShortFormat,
- 0);
+ useShortFormat);
}
/**
@@ -400,7 +405,7 @@ public class Utils {
long startUtcMillis,
long endUtcMillis,
boolean useShortFormat,
- int flag) {
+ int flags) {
return getDurationString(
context,
startUtcMillis,
@@ -408,7 +413,7 @@ public class Utils {
useShortFormat,
!isInGivenDay(baseMillis, startUtcMillis),
true,
- flag);
+ flags);
}
/**
@@ -422,16 +427,20 @@ public class Utils {
boolean useShortFormat,
boolean showDate,
boolean showTime,
- int flag) {
- flag |=
+ int flags) {
+ flags |=
DateUtils.FORMAT_ABBREV_MONTH
| ((useShortFormat) ? DateUtils.FORMAT_NUMERIC_DATE : 0);
SoftPreconditions.checkArgument(showTime || showDate);
if (showTime) {
- flag |= DateUtils.FORMAT_SHOW_TIME;
+ flags |= DateUtils.FORMAT_SHOW_TIME;
}
if (showDate) {
- flag |= DateUtils.FORMAT_SHOW_DATE;
+ flags |= DateUtils.FORMAT_SHOW_DATE;
+ }
+ if (!showDate || (flags & DateUtils.FORMAT_SHOW_YEAR) == 0) {
+ // year is not shown unless DateUtils.FORMAT_SHOW_YEAR is set explicitly
+ flags |= DateUtils.FORMAT_NO_YEAR;
}
if (startUtcMillis != endUtcMillis && useShortFormat) {
// Do special handling for 12:00 AM when checking if it's in the given day.
@@ -443,15 +452,15 @@ public class Utils {
// Subtracting one day is needed because {@link DateUtils@formatDateRange}
// automatically shows date if the duration covers multiple days.
return DateUtils.formatDateRange(
- context, startUtcMillis, endUtcMillis - TimeUnit.DAYS.toMillis(1), flag);
+ context, startUtcMillis, endUtcMillis - TimeUnit.DAYS.toMillis(1), flags);
}
}
// Workaround of b/28740989.
// Add 1 msec to endUtcMillis to avoid DateUtils' bug with a duration of 12:00AM~12:00AM.
- String dateRange = DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis, flag);
+ String dateRange = DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis, flags);
return startUtcMillis == endUtcMillis || dateRange.contains("–")
? dateRange
- : DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flag);
+ : DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flags);
}
/**
@@ -572,86 +581,6 @@ public class Utils {
return "";
}
- public static boolean needToShowSampleRate(Context context, List<TvTrackInfo> tracks) {
- Set<String> multiAudioStrings = new HashSet<>();
- for (TvTrackInfo track : tracks) {
- String multiAudioString = getMultiAudioString(context, track, false);
- if (multiAudioStrings.contains(multiAudioString)) {
- return true;
- }
- multiAudioStrings.add(multiAudioString);
- }
- return false;
- }
-
- public static String getMultiAudioString(
- Context context, TvTrackInfo track, boolean showSampleRate) {
- if (track.getType() != TvTrackInfo.TYPE_AUDIO) {
- throw new IllegalArgumentException("Not an audio track: " + track);
- }
- String language = context.getString(R.string.multi_audio_unknown_language);
- if (!TextUtils.isEmpty(track.getLanguage())) {
- language = new Locale(track.getLanguage()).getDisplayName();
- } else {
- Log.d(TAG, "No language information found for the audio track: " + track);
- }
-
- StringBuilder metadata = new StringBuilder();
- switch (track.getAudioChannelCount()) {
- case AUDIO_CHANNEL_NONE:
- break;
- case AUDIO_CHANNEL_MONO:
- metadata.append(context.getString(R.string.multi_audio_channel_mono));
- break;
- case AUDIO_CHANNEL_STEREO:
- metadata.append(context.getString(R.string.multi_audio_channel_stereo));
- break;
- case AUDIO_CHANNEL_SURROUND_6:
- metadata.append(context.getString(R.string.multi_audio_channel_surround_6));
- break;
- case AUDIO_CHANNEL_SURROUND_8:
- metadata.append(context.getString(R.string.multi_audio_channel_surround_8));
- break;
- default:
- if (track.getAudioChannelCount() > 0) {
- metadata.append(
- context.getString(
- R.string.multi_audio_channel_suffix,
- track.getAudioChannelCount()));
- } else {
- Log.d(
- TAG,
- "Invalid audio channel count ("
- + track.getAudioChannelCount()
- + ") found for the audio track: "
- + track);
- }
- break;
- }
- if (showSampleRate) {
- int sampleRate = track.getAudioSampleRate();
- if (sampleRate > 0) {
- if (metadata.length() > 0) {
- metadata.append(", ");
- }
- int integerPart = sampleRate / 1000;
- int tenths = (sampleRate % 1000) / 100;
- metadata.append(integerPart);
- if (tenths != 0) {
- metadata.append(".");
- metadata.append(tenths);
- }
- metadata.append("kHz");
- }
- }
-
- if (metadata.length() == 0) {
- return language;
- }
- return context.getString(
- R.string.multi_audio_display_string_with_channel, language, metadata.toString());
- }
-
public static boolean isEqualLanguage(String lang1, String lang2) {
if (lang1 == null) {
return lang2 == null;
@@ -708,7 +637,6 @@ public class Utils {
if (fullFormat) {
return new Date(timeMillis).toString();
} else {
- long currentTime = System.currentTimeMillis();
return (String)
DateUtils.formatSameDayTime(
timeMillis,
@@ -815,8 +743,11 @@ public class Utils {
/** Checks whether the input is internal or not. */
public static boolean isInternalTvInput(Context context, String inputId) {
- return context.getPackageName()
- .equals(ComponentName.unflattenFromString(inputId).getPackageName());
+ ComponentName unflattenInputId = ComponentName.unflattenFromString(inputId);
+ if (unflattenInputId == null) {
+ return false;
+ }
+ return context.getPackageName().equals(unflattenInputId.getPackageName());
}
/** Returns the TV input for the given {@code program}. */
diff --git a/src/com/android/tv/util/images/BitmapUtils.java b/src/com/android/tv/util/images/BitmapUtils.java
index d6bd5a31..39524503 100644
--- a/src/com/android/tv/util/images/BitmapUtils.java
+++ b/src/com/android/tv/util/images/BitmapUtils.java
@@ -20,13 +20,16 @@ import android.content.ContentResolver;
import android.content.Context;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.TrafficStats;
import android.net.Uri;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import com.android.tv.common.util.NetworkTrafficTags;
@@ -88,6 +91,19 @@ public final class BitmapUtils {
calculateInSampleSize(bm.getWidth(), bm.getHeight(), maxWidth, maxHeight));
}
+ @Nullable
+ public static Bitmap drawableToBitmap(Drawable drawable) {
+ if (drawable == null) {
+ return null;
+ }
+ Bitmap bm = Bitmap.createBitmap(
+ drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Config.ARGB_8888);
+ Canvas canvas = new Canvas(bm);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ return bm;
+ }
+
/** Decode large sized bitmap into requested size. */
public static ScaledBitmapInfo decodeSampledBitmapFromUriString(
Context context, String uriString, int reqWidth, int reqHeight) {
diff --git a/src/com/android/tv/util/images/ImageLoader.java b/src/com/android/tv/util/images/ImageLoader.java
index e844e2ca..d2ad0eb1 100644
--- a/src/com/android/tv/util/images/ImageLoader.java
+++ b/src/com/android/tv/util/images/ImageLoader.java
@@ -24,7 +24,6 @@ import android.media.tv.TvInputInfo;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
-import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.annotation.WorkerThread;
@@ -145,22 +144,14 @@ public final class ImageLoader {
final Context appContext = context.getApplicationContext();
getMainHandler()
.post(
- new Runnable() {
- @Override
- @MainThread
- public void run() {
- // Calling from the main thread prevents a
- // ConcurrentModificationException
- // in LoadBitmapTask.onPostExecute
+ () ->
doLoadBitmap(
appContext,
uriString,
maxWidth,
maxHeight,
null,
- AsyncTask.SERIAL_EXECUTOR);
- }
- });
+ AsyncTask.SERIAL_EXECUTOR));
}
}
@@ -423,14 +414,12 @@ public final class ImageLoader {
@Override
public ScaledBitmapInfo doGetBitmapInBackground() {
Drawable drawable = mInfo.loadIcon(mAppContext);
- if (!(drawable instanceof BitmapDrawable)) {
- return null;
- }
- Bitmap original = ((BitmapDrawable) drawable).getBitmap();
- if (original == null) {
- return null;
- }
- return BitmapUtils.createScaledBitmapInfo(getKey(), original, mMaxWidth, mMaxHeight);
+ Bitmap bm = drawable instanceof BitmapDrawable
+ ? ((BitmapDrawable) drawable).getBitmap()
+ : BitmapUtils.drawableToBitmap(drawable);
+ return bm == null
+ ? null
+ : BitmapUtils.createScaledBitmapInfo(getKey(), bm, mMaxWidth, mMaxHeight);
}
/** Returns key of TV input logo. */