diff options
Diffstat (limited to 'src/com/android')
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. */ |