diff options
Diffstat (limited to 'src')
113 files changed, 5734 insertions, 2565 deletions
diff --git a/src/com/android/tv/ChannelTuner.java b/src/com/android/tv/ChannelTuner.java index 0a000e9b..faa27bbd 100644 --- a/src/com/android/tv/ChannelTuner.java +++ b/src/com/android/tv/ChannelTuner.java @@ -22,12 +22,12 @@ import android.net.Uri; import android.os.Handler; import android.support.annotation.MainThread; import android.support.annotation.Nullable; +import android.util.ArraySet; import android.util.Log; -import com.android.tv.common.CollectionUtils; +import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; -import com.android.tv.util.SoftPreconditions; import com.android.tv.util.TvInputManagerHelper; import java.util.ArrayList; @@ -56,7 +56,7 @@ public class ChannelTuner { private final Handler mHandler = new Handler(); private final ChannelDataManager mChannelDataManager; - private final Set<Listener> mListeners = CollectionUtils.createSmallSet(); + private final Set<Listener> mListeners = new ArraySet<>(); @Nullable private Channel mCurrentChannel; private final TvInputManagerHelper mInputManager; diff --git a/src/com/android/tv/Features.java b/src/com/android/tv/Features.java index 1a665506..6a78b632 100644 --- a/src/com/android/tv/Features.java +++ b/src/com/android/tv/Features.java @@ -16,20 +16,21 @@ package com.android.tv; +import static com.android.tv.common.feature.EngOnlyFeature.ENG_ONLY_FEATURE; +import static com.android.tv.common.feature.FeatureUtils.AND; +import static com.android.tv.common.feature.FeatureUtils.ON; +import static com.android.tv.common.feature.FeatureUtils.OR; + +import android.content.Context; +import android.os.Build; import android.support.annotation.VisibleForTesting; +import android.support.v4.os.BuildCompat; import com.android.tv.common.feature.Feature; import com.android.tv.common.feature.GServiceFeature; import com.android.tv.common.feature.PackageVersionFeature; import com.android.tv.common.feature.PropertyFeature; -import com.android.tv.common.feature.SharedPreferencesFeature; -import com.android.tv.common.feature.TestableFeature; - -import static com.android.tv.common.feature.FeatureUtils.AND; -import static com.android.tv.common.feature.FeatureUtils.ON; -import static com.android.tv.common.feature.FeatureUtils.OR; -import static com.android.tv.common.feature.TestableFeature.createTestableFeature; -import static com.android.tv.common.feature.EngOnlyFeature.ENG_ONLY_FEATURE; +import com.android.tv.util.PermissionUtils; /** * List of {@link Feature} for the Live TV App. @@ -43,47 +44,65 @@ public final class Features { * <p>Do not turn this on until the splash screen asking existing users to opt-in is launched. * See <a href="http://b/20228119">b/20228119</a> */ - public static Feature ANALYTICS_OPT_IN = ENG_ONLY_FEATURE; + public static final Feature ANALYTICS_OPT_IN = ENG_ONLY_FEATURE; /** * Analytics that include sensitive information such as channel or program identifiers. * * <p>See <a href="http://b/22062676">b/22062676</a> */ - public static Feature ANALYTICS_V2 = AND(ON, ANALYTICS_OPT_IN); + public static final Feature ANALYTICS_V2 = AND(ON, ANALYTICS_OPT_IN); - public static Feature EPG_SEARCH = new PropertyFeature("feature_tv_use_epg_search", false); + public static final Feature EPG_SEARCH = + new PropertyFeature("feature_tv_use_epg_search", false); - public static SharedPreferencesFeature USB_TUNER = new SharedPreferencesFeature( - "usb_tuner", true, - OR(ENG_ONLY_FEATURE, new GServiceFeature("usbtuner_enabled", false))); - public static Feature DEVELOPER_OPTION = OR(ENG_ONLY_FEATURE, - new GServiceFeature("usbtuner_enabled", false)); + public static final Feature USB_TUNER = new Feature() { + + /** + * This is special handling just for USB Tuner. + * It does not require any N API's but relies on a improvements in N for AC3 support + * After release, change class to this to just be + * {@link BuildCompat#isAtLeastN()}. + */ + @Override + public boolean isEnabled(Context context) { + return Build.VERSION.SDK_INT > Build.VERSION_CODES.M || BuildCompat.isAtLeastN(); + } + + }; private static final String PLAY_STORE_PACKAGE_NAME = "com.android.vending"; private static final int PLAY_STORE_ZIMA_VERSION_CODE = 80441186; - private static Feature PLAY_STORE_LINK = new PackageVersionFeature(PLAY_STORE_PACKAGE_NAME, - PLAY_STORE_ZIMA_VERSION_CODE); + private static final Feature PLAY_STORE_LINK = + new PackageVersionFeature(PLAY_STORE_PACKAGE_NAME, PLAY_STORE_ZIMA_VERSION_CODE); - public static Feature ONBOARDING_PLAY_STORE = PLAY_STORE_LINK; + public static final Feature ONBOARDING_PLAY_STORE = PLAY_STORE_LINK; /** * A flag which indicates that the on-boarding experience is used or not. * * <p>See <a href="http://b/24070322">b/24070322</a> */ - public static Feature ONBOARDING_EXPERIENCE = ONBOARDING_PLAY_STORE; + public static final Feature ONBOARDING_EXPERIENCE = ONBOARDING_PLAY_STORE; private static final String GSERVICE_KEY_UNHIDE = "live_channels_unhide"; /** * A flag which indicates that LC app is unhidden even when there is no input. */ - public static Feature UNHIDE = AND(ONBOARDING_EXPERIENCE, - new GServiceFeature(GSERVICE_KEY_UNHIDE, false)); + public static final Feature UNHIDE = AND(ONBOARDING_EXPERIENCE, + OR(new GServiceFeature(GSERVICE_KEY_UNHIDE, false), new Feature() { + @Override + public boolean isEnabled(Context context) { + // If LC app runs as non-system app, we unhide the app. + return !PermissionUtils.hasAccessAllEpg(context); + } + })); @VisibleForTesting public static Feature TEST_FEATURE = new PropertyFeature("test_feature", false); + public static final Feature FETCH_EPG = new PropertyFeature("live_channels_fetch_epg", false); + private Features() { } } diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java index 99bcb125..78fda42a 100644 --- a/src/com/android/tv/MainActivity.java +++ b/src/com/android/tv/MainActivity.java @@ -40,6 +40,7 @@ import android.media.tv.TvContract; import android.media.tv.TvContract.Channels; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; +import android.media.tv.TvInputManager.TvInputCallback; import android.media.tv.TvTrackInfo; import android.media.tv.TvView.OnUnhandledInputEventListener; import android.net.Uri; @@ -52,6 +53,7 @@ import android.provider.Settings; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.os.BuildCompat; import android.text.TextUtils; import android.util.Log; import android.view.Display; @@ -73,10 +75,12 @@ import com.android.tv.analytics.SendConfigInfoRunnable; import com.android.tv.analytics.Tracker; import com.android.tv.common.BuildConfig; import com.android.tv.common.MemoryManageable; +import com.android.tv.common.SoftPreconditions; import com.android.tv.common.TvCommonUtils; import com.android.tv.common.TvContentRatingCache; import com.android.tv.common.WeakHandler; import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.common.recording.RecordedProgram; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.OnCurrentProgramUpdatedListener; @@ -89,7 +93,7 @@ import com.android.tv.dialog.SafeDismissDialogFragment; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrPlayActivity; -import com.android.tv.dvr.Recording; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.menu.Menu; import com.android.tv.onboarding.OnboardingActivity; import com.android.tv.parental.ContentRatingsManager; @@ -125,11 +129,13 @@ import com.android.tv.util.PipInputManager.PipInput; import com.android.tv.util.RecurringRunner; import com.android.tv.util.SearchManagerHelper; import com.android.tv.util.SetupUtils; -import com.android.tv.util.SoftPreconditions; import com.android.tv.util.SystemProperties; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.TvSettings; import com.android.tv.util.TvSettings.PipSound; +import com.android.usbtuner.UsbTunerPreferences; +import com.android.usbtuner.setup.TunerSetupActivity; +import com.android.usbtuner.tvinput.UsbTunerTvInputService; import com.android.tv.util.TvTrackInfoUtils; import com.android.tv.util.Utils; @@ -140,7 +146,6 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Objects; -import java.util.Set; import java.util.concurrent.TimeUnit; /** @@ -268,6 +273,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private MediaSession mMediaSession; private int mNowPlayingCardWidth; private int mNowPlayingCardHeight; + private final MyOnTuneListener mOnTuneListener = new MyOnTuneListener(); private String mInputIdUnderSetup; private boolean mIsSetupActivityCalledByPopup; @@ -281,7 +287,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private boolean mDebugNonFullSizeScreen; private boolean mActivityResumed; private boolean mActivityStarted; - private boolean mLaunchedByLauncher; + private boolean mShouldTuneToTunerChannel; private boolean mUseKeycodeBlacklist; private boolean mShowLockedChannelsTemporarily; private boolean mBackKeyPressed; @@ -290,6 +296,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private boolean mAc3PassthroughSupported; private boolean mShowNewSourcesFragment = true; private Uri mRecordingUri; + private String mUsbTunerInputId; + private boolean mOtherActivityLaunched; private boolean mIsFilmModeSet; private float mDefaultRefreshRate; @@ -323,7 +331,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC // A caller which started this activity. (e.g. TvSearch) private String mSource; - private Handler mHandler = new MainActivityHandler(this); + private final Handler mHandler = new MainActivityHandler(this); private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override @@ -367,6 +375,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC Channel channel = mTvView.getCurrentChannel(); if (channel != null && channel.getId() == channelId) { updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO); + updateMediaSession(); } } }; @@ -413,6 +422,19 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC }; private ProgramGuideSearchFragment mSearchFragment; + private TvInputCallback mTvInputCallback = new TvInputCallback() { + @Override + public void onInputAdded(String inputId) { + if (mUsbTunerInputId.equals(inputId) + && UsbTunerPreferences.shouldShowSetupActivity(MainActivity.this)) { + Intent intent = TunerSetupActivity.createSetupActivity(MainActivity.this); + startActivity(intent); + UsbTunerPreferences.setShouldShowSetupActivity(MainActivity.this, false); + SetupUtils.getInstance(MainActivity.this).markAsKnownInput(mUsbTunerInputId); + } + } + }; + private void applyParentalControlSettings() { boolean parentalControlEnabled = mTvInputManagerHelper.getParentalControlSettings() .isParentalControlsEnabled(); @@ -424,12 +446,19 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC protected void onCreate(Bundle savedInstanceState) { if (DEBUG) Log.d(TAG,"onCreate()"); super.onCreate(savedInstanceState); - + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M + && !PermissionUtils.hasAccessAllEpg(this)) { + Toast.makeText(this, R.string.msg_not_supported_device, Toast.LENGTH_LONG).show(); + finish(); + return; + } + boolean skipToShowOnboarding = getIntent().getAction() == Intent.ACTION_VIEW + && TvContract.isChannelUriForPassthroughInput(getIntent().getData()); if (Features.ONBOARDING_EXPERIENCE.isEnabled(this) - && OnboardingUtils.needToShowOnboarding(this) + && OnboardingUtils.needToShowOnboarding(this) && !skipToShowOnboarding && !TvCommonUtils.isRunningInTest()) { - // TODO: We turn off the new onboarding for test, because tests are broken by - // the new onboarding. We need to enable the feature for tests later. + // TODO: The onboarding is turned off in test, because tests are broken by the + // onboarding. We need to enable the feature for tests later. startActivity(OnboardingActivity.buildIntent(this, getIntent())); finish(); return; @@ -442,6 +471,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } mTracker = tvApplication.getTracker(); mTvInputManagerHelper = tvApplication.getTvInputManagerHelper(); + mTvInputManagerHelper.addCallback(mTvInputCallback); + mUsbTunerInputId = UsbTunerTvInputService.getInputId(this); mChannelDataManager = tvApplication.getChannelDataManager(); mProgramDataManager = tvApplication.getProgramDataManager(); mProgramDataManager.addOnCurrentProgramUpdatedListener(Channel.INVALID_ID, @@ -455,7 +486,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mMemoryManageables.add(mProgramDataManager); mMemoryManageables.add(ImageCache.getInstance()); mMemoryManageables.add(TvContentRatingCache.getInstance()); - if(CommonFeatures.DVR.isEnabled(this)) { + if (CommonFeatures.DVR.isEnabled(this) && BuildCompat.isAtLeastN()) { mDvrManager = tvApplication.getDvrManager(); mDvrDataManager = tvApplication.getDvrDataManager(); } @@ -502,6 +533,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC new OnCurrentProgramUpdatedListener() { @Override public void onCurrentProgramUpdated(long channelId, Program program) { + updateMediaSession(); switch (mTimeShiftManager.getLastActionId()) { case TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND: case TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD: @@ -618,6 +650,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mSendConfigInfoRecurringRunner.start(); mChannelStatusRecurringRunner = SendChannelStatusRunnable .startChannelStatusRecurringRunner(this, mTracker, mChannelDataManager); + + // To avoid not updating Rating systems when changing language. + mTvInputManagerHelper.getContentRatingsManager().update(); + initForTest(); } @@ -625,7 +661,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (requestCode == PERMISSIONS_REQUEST_READ_TV_LISTINGS) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (grantResults != null && grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Start reload of dependent data + mChannelDataManager.reload(); + mProgramDataManager.reload(); + // Restart live channels. Intent intent = getIntent(); finish(); @@ -719,15 +760,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC protected void onResume() { if (DEBUG) Log.d(TAG, "onResume()"); super.onResume(); - if (!PermissionUtils.hasAccessAllEpg(this)) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - Toast.makeText(this, R.string.msg_not_supported_device, Toast.LENGTH_LONG).show(); - finish(); - } else if (checkSelfPermission(PERMISSION_READ_TV_LISTINGS) + if (!PermissionUtils.hasAccessAllEpg(this) + && checkSelfPermission(PERMISSION_READ_TV_LISTINGS) != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[]{PERMISSION_READ_TV_LISTINGS}, - PERMISSIONS_REQUEST_READ_TV_LISTINGS); - } + requestPermissions(new String[]{PERMISSION_READ_TV_LISTINGS}, + PERMISSIONS_REQUEST_READ_TV_LISTINGS); } mTracker.sendScreenView(SCREEN_NAME); @@ -735,6 +772,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mNeedShowBackKeyGuide = true; mActivityResumed = true; mShowNewSourcesFragment = true; + mOtherActivityLaunched = false; int result = mAudioManager.requestAudioFocus(MainActivity.this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); mAudioFocusStatus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ? @@ -798,7 +836,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } mBackKeyPressed = false; mShowLockedChannelsTemporarily = false; - mLaunchedByLauncher = false; + mShouldTuneToTunerChannel = false; if (!mVisibleBehind) { mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; mAudioManager.abandonAudioFocus(this); @@ -836,7 +874,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private void resumeTvIfNeeded() { if (DEBUG) Log.d(TAG, "resumeTvIfNeeded()"); if (!mTvView.isPlaying() || mInitChannelUri != null - || (mLaunchedByLauncher && mChannelTuner.isCurrentChannelPassthrough())) { + || (mShouldTuneToTunerChannel && mChannelTuner.isCurrentChannelPassthrough())) { if (TvContract.isChannelUriForPassthroughInput(mInitChannelUri)) { // The target input may not be ready yet, especially, just after screen on. String inputId = mInitChannelUri.getPathSegments().get(1); @@ -1079,10 +1117,15 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } public Channel getCurrentChannel() { - return mChannelTuner.getCurrentChannel(); + return mTvView.isRecordingPlayback() ? mTvView.getCurrentChannel() + : mChannelTuner.getCurrentChannel(); } public long getCurrentChannelId() { + if (mTvView.isRecordingPlayback()) { + Channel channel = mTvView.getCurrentChannel(); + return channel == null ? Channel.INVALID_ID : channel.getId(); + } return mChannelTuner.getCurrentChannelId(); } @@ -1099,7 +1142,31 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC * If the time shifting is available, it can be a past program. */ public Program getCurrentProgram() { - if (mTimeShiftManager.isAvailable()) { + return getCurrentProgram(true); + } + + /** + * Returns {@code true}, if this view is the recording playback mode. + */ + public boolean isRecordingPlayback() { + return mTvView.isRecordingPlayback(); + } + + /** + * Returns the recording which is being played right now. + */ + public RecordedProgram getPlayingRecordedProgram() { + return mTvView.getPlayingRecordedProgram(); + } + + /** + * Returns the current program which the user is watching right now.<p> + * + * @param applyTimeShifted If it is true and the time shifting is available, it can be + * a past program. + */ + public Program getCurrentProgram(boolean applyTimeShifted) { + if (applyTimeShifted && mTimeShiftManager.isAvailable()) { return mTimeShiftManager.getCurrentProgram(); } return mProgramDataManager.getCurrentProgram(getCurrentChannelId()); @@ -1372,7 +1439,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC onKeyUp(keyCode, event); return true; } - mLaunchedByLauncher = intent.getBooleanExtra(Utils.EXTRA_KEY_FROM_LAUNCHER, false); + mShouldTuneToTunerChannel = intent.getBooleanExtra(Utils.EXTRA_KEY_FROM_LAUNCHER, false); mInitChannelUri = null; String extraAction = intent.getStringExtra(Utils.EXTRA_KEY_ACTION); @@ -1387,14 +1454,24 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } - if (CommonFeatures.DVR.isEnabled(this)) { + if (CommonFeatures.DVR.isEnabled(this) && BuildCompat.isAtLeastN()) { mRecordingUri = intent.getParcelableExtra(Utils.EXTRA_KEY_RECORDING_URI); if (mRecordingUri != null) { return true; } } - if (Intent.ACTION_VIEW.equals(intent.getAction())) { + // TODO: remove the checkState once N API is finalized. + SoftPreconditions.checkState(TvInputManager.ACTION_SETUP_INPUTS.equals( + "android.media.tv.action.SETUP_INPUTS")); + if (TvInputManager.ACTION_SETUP_INPUTS.equals(intent.getAction())) { + runAfterAttachedToWindow(new Runnable() { + @Override + public void run() { + mOverlayManager.showSetupFragment(); + } + }); + } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { Uri uri = intent.getData(); try { mSource = uri.getQueryParameter(Utils.PARAM_SOURCE); @@ -1416,6 +1493,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (Channels.CONTENT_URI.equals(mInitChannelUri)) { // Tune to default channel. mInitChannelUri = null; + mShouldTuneToTunerChannel = true; return true; } if ((!Utils.isChannelUriForOneChannel(mInitChannelUri) @@ -1483,6 +1561,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } private void setVolumeByAudioFocusStatus(TunableTvView tvView) { + SoftPreconditions.checkState(tvView == mTvView || tvView == mPipView); if (tvView.isPlaying()) { switch (mAudioFocusStatus) { case AudioManager.AUDIOFOCUS_GAIN: @@ -1497,6 +1576,15 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC break; } } + if (tvView == mTvView) { + if (mPipView != null && mPipView.isPlaying()) { + mPipView.setStreamVolume(AUDIO_MIN_VOLUME); + } + } else { // tvView == mPipView + if (mTvView != null && mTvView.isPlaying()) { + mTvView.setStreamVolume(AUDIO_MIN_VOLUME); + } + } } private void stopTv() { @@ -1601,7 +1689,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mPipView.setMain(); scheduleRestoreMainTvView(); mTvViewUiManager.onPipStart(); - mPipView.setStreamVolume(AUDIO_MIN_VOLUME); + setVolumeByAudioFocusStatus(); } private void scheduleRestoreMainTvView() { @@ -1633,27 +1721,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } private void playRecording(Uri recordingUri) { - String inputId = recordingUri.getQueryParameter(Recording.PARAM_INPUT_ID); - SoftPreconditions.checkNotNull(inputId); - mTvView.playRecording(inputId, recordingUri, new OnTuneListener() { - @Override - public void onTuneFailed(Channel channel) { } - - @Override - public void onUnexpectedStop(Channel channel) { } - - @Override - public void onStreamInfoChanged(StreamInfo info) { } - - @Override - public void onChannelRetuned(Uri channel) { } - - @Override - public void onContentBlocked() { } - - @Override - public void onContentAllowed() { } - }); + mTvView.playRecording(recordingUri, mOnTuneListener); + mOnTuneListener.onPlayRecording(); + updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE); } private void tune() { @@ -1668,75 +1738,73 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return; } mTunePending = false; - if (!mChannelTuner.isCurrentChannelPassthrough() - && mTvInputManagerHelper.getTunerTvInputSize() == 0) { - Toast.makeText(this, R.string.msg_no_input, Toast.LENGTH_SHORT).show(); - // TODO: Direct the user to a Play Store landing page for TvInputService apps. - finish(); - return; - } - SetupUtils setupUtils = SetupUtils.getInstance(this); - if (!mChannelTuner.isCurrentChannelPassthrough() && setupUtils.isFirstTune()) { - if (!mChannelTuner.areAllChannelsLoaded()) { - // tune() will be called, once all channels are loaded. - stopTv("tune()", false); - return; - } - if (mChannelDataManager.getChannelCount() > 0) { - mOverlayManager.showIntroDialog(); - } else if (!Features.ONBOARDING_EXPERIENCE.isEnabled(this)) { - mOverlayManager.showSetupFragment(); + final Channel channel = mChannelTuner.getCurrentChannel(); + if (!mChannelTuner.isCurrentChannelPassthrough()) { + if (mTvInputManagerHelper.getTunerTvInputSize() == 0) { + Toast.makeText(this, R.string.msg_no_input, Toast.LENGTH_SHORT).show(); + // TODO: Direct the user to a Play Store landing page for TvInputService apps. + finish(); return; } - } - if (!TvCommonUtils.isRunningInTest() && mShowNewSourcesFragment - && setupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) { - // Show new channel sources fragment. - runAfterAttachedToWindow(new Runnable() { - @Override - public void run() { - mOverlayManager.runAfterOverlaysAreClosed(new Runnable() { - @Override - public void run() { - mOverlayManager.showNewSourcesFragment(); - } - }); + SetupUtils setupUtils = SetupUtils.getInstance(this); + if (setupUtils.isFirstTune()) { + if (!mChannelTuner.areAllChannelsLoaded()) { + // tune() will be called, once all channels are loaded. + stopTv("tune()", false); + return; + } + if (mChannelDataManager.getChannelCount() > 0) { + mOverlayManager.showIntroDialog(); + } else if (!Features.ONBOARDING_EXPERIENCE.isEnabled(this)) { + mOverlayManager.showSetupFragment(); + return; } - }); - } - mShowNewSourcesFragment = false; - if (!mChannelTuner.isCurrentChannelPassthrough() - && mChannelTuner.getBrowsableChannelCount() == 0 - && mChannelDataManager.getChannelCount() > 0 - && !mOverlayManager.getSideFragmentManager().isActive()) { - if (!mChannelTuner.areAllChannelsLoaded()) { - return; } - if (mTvInputManagerHelper.getTunerTvInputSize() == 1) { - mOverlayManager.getSideFragmentManager().show(new CustomizeChannelListFragment()); - } else { - showSettingsFragment(); + if (!TvCommonUtils.isRunningInTest() && mShowNewSourcesFragment + && setupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) { + // Show new channel sources fragment. + runAfterAttachedToWindow(new Runnable() { + @Override + public void run() { + mOverlayManager.runAfterOverlaysAreClosed(new Runnable() { + @Override + public void run() { + mOverlayManager.showNewSourcesFragment(); + } + }); + } + }); } - return; - } - // TODO: need to refactor the following code to put in startTv. - final Channel channel = mChannelTuner.getCurrentChannel(); - if (channel == null) { - // There is no channel to tune to. - stopTv("tune()", false); - if (!mChannelDataManager.isDbLoadFinished()) { - // Wait until channel data is loaded in order to know the number of channels. - // tune() will be retried, once the channel data is loaded. + mShowNewSourcesFragment = false; + if (mChannelTuner.getBrowsableChannelCount() == 0 + && mChannelDataManager.getChannelCount() > 0 + && !mOverlayManager.getSideFragmentManager().isActive()) { + if (!mChannelTuner.areAllChannelsLoaded()) { + return; + } + if (mTvInputManagerHelper.getTunerTvInputSize() == 1) { + mOverlayManager.getSideFragmentManager().show( + new CustomizeChannelListFragment()); + } else { + showSettingsFragment(); + } return; } - if (mOverlayManager.getSideFragmentManager().isActive()) { + // TODO: need to refactor the following code to put in startTv. + if (channel == null) { + // There is no channel to tune to. + stopTv("tune()", false); + if (!mChannelDataManager.isDbLoadFinished()) { + // Wait until channel data is loaded in order to know the number of channels. + // tune() will be retried, once the channel data is loaded. + return; + } + if (mOverlayManager.getSideFragmentManager().isActive()) { + return; + } + mOverlayManager.showSetupFragment(); return; } - mOverlayManager.showSetupFragment(); - return; - } - - if (!channel.isPassthrough()) { setupUtils.onTuned(); if (mTuneParams != null) { Long initChannelId = mTuneParams.getLong(KEY_INIT_CHANNEL_ID); @@ -1752,9 +1820,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (!isUnderShrunkenTvView()) { mLastAllowedRatingForCurrentChannel = null; } - final boolean wasUnderShrunkenTvView = isUnderShrunkenTvView(); - final long streamInfoUpdateTimeThresholdMs = - System.currentTimeMillis() + FIRST_STREAM_INFO_UPDATE_DELAY_MILLIS; mHandler.removeMessages(MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE); if (mAccessibilityManager.isEnabled()) { // For every tune, we need to inform the tuned channel or input to a user, @@ -1774,105 +1839,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mAccessibilityManager.sendAccessibilityEvent(event); } - boolean success = mTvView.tuneTo(channel, mTuneParams, new OnTuneListener() { - boolean mUnlockAllowedRatingBeforeShrunken = true; - - @Override - public void onUnexpectedStop(Channel channel) { - stopTv(); - startTv(null); - } - - @Override - public void onTuneFailed(Channel channel) { - Log.w(TAG, "Failed to tune to channel " + channel.getId() - + "@" + channel.getInputId()); - if (mTvView.isFadedOut()) { - mTvView.removeFadeEffect(); - } - // TODO: show something to user about this error. - } + boolean success = mTvView.tuneTo(channel, mTuneParams, mOnTuneListener); + mOnTuneListener.onTune(channel, isUnderShrunkenTvView()); - @Override - public void onStreamInfoChanged(StreamInfo info) { - if (info.isVideoAvailable() && mTuneDurationTimer.isRunning()) { - mTracker.sendChannelTuneTime(info.getCurrentChannel(), - mTuneDurationTimer.reset()); - } - // If updateChannelBanner() is called without delay, the stream info seems flickering - // when the channel is quickly changed. - if (!mHandler.hasMessages(MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE) - && info.isVideoAvailable()) { - if (System.currentTimeMillis() > streamInfoUpdateTimeThresholdMs) { - updateChannelBannerAndShowIfNeeded( - UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO); - } else { - mHandler.sendMessageDelayed(mHandler.obtainMessage( - MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE), - streamInfoUpdateTimeThresholdMs - System.currentTimeMillis()); - } - } - - applyDisplayRefreshRate(info.getVideoFrameRate()); - mTvViewUiManager.updateTvView(); - applyMultiAudio(); - applyClosedCaption(); - // TODO: Send command to TIS with checking the settings in TV and CaptionManager. - mOverlayManager.getMenu().onStreamInfoChanged(); - if (mTvView.isVideoAvailable()) { - mTvViewUiManager.fadeInTvView(); - } - mHandler.removeCallbacks(mRestoreMainViewRunnable); - restoreMainTvView(); - } - - @Override - public void onChannelRetuned(Uri channel) { - if (channel == null) { - return; - } - Channel currentChannel = - mChannelDataManager.getChannel(ContentUris.parseId(channel)); - if (currentChannel == null) { - Log.e(TAG, "onChannelRetuned is called but can't find a channel with the URI " - + channel); - return; - } - if (isChannelChangeKeyDownReceived()) { - // Ignore this message if the user is changing the channel. - return; - } - mChannelTuner.setCurrentChannel(currentChannel); - mTvView.setCurrentChannel(currentChannel); - updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE); - } - - @Override - public void onContentBlocked() { - mTuneDurationTimer.reset(); - TvContentRating rating = mTvView.getBlockedContentRating(); - // When tuneTo was called while TV view was shrunken, if the channel id is the same - // with the channel watched before shrunken, we allow the rating which was allowed - // before. - if (wasUnderShrunkenTvView && mUnlockAllowedRatingBeforeShrunken - && mChannelBeforeShrunkenTvView.equals(channel) - && rating.equals(mAllowedRatingBeforeShrunken)) { - mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView(); - mTvView.requestUnblockContent(rating); - } - - updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); - mTvViewUiManager.fadeInTvView(); - } - - @Override - public void onContentAllowed() { - if (!isUnderShrunkenTvView()) { - mUnlockAllowedRatingBeforeShrunken = false; - } - updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); - } - }); mTuneParams = null; if (!success) { Toast.makeText(this, R.string.msg_tune_failed, Toast.LENGTH_SHORT).show(); @@ -1969,6 +1938,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } private void updateProgramPosterArt(Program program, @Nullable Bitmap posterArt) { + if (getCurrentChannel() == null) { + return; + } if (posterArt != null) { String cardTitleText = program == null ? null : program.getTitle(); if (TextUtils.isEmpty(cardTitleText)) { @@ -1995,9 +1967,15 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return; } - String cardTitleText = program == null ? null : program.getTitle(); - if (TextUtils.isEmpty(cardTitleText)) { - cardTitleText = channel.getDisplayName(); + String cardTitleText; + if (channel.isPassthrough()) { + TvInputInfo input = getTvInputManagerHelper().getTvInputInfo(channel.getInputId()); + cardTitleText = Utils.loadLabel(this, input); + } else { + cardTitleText = program == null ? null : program.getTitle(); + if (TextUtils.isEmpty(cardTitleText)) { + cardTitleText = channel.getDisplayName(); + } } Bitmap posterArt = BitmapFactory.decodeResource( @@ -2077,7 +2055,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private void updateChannelBannerAndShowIfNeeded(@ChannelBannerUpdateReason int reason) { if(DEBUG) Log.d(TAG, "updateChannelBannerAndShowIfNeeded(reason=" + reason + ")"); - if (!mChannelTuner.isCurrentChannelPassthrough()) { + if (!mChannelTuner.isCurrentChannelPassthrough() || mTvView.isRecordingPlayback()) { int lockType = ChannelBannerView.LOCK_NONE; if (mTvView.isScreenBlocked()) { lockType = ChannelBannerView.LOCK_CHANNEL_INFO; @@ -2321,6 +2299,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mChannelStatusRecurringRunner.stop(); mChannelStatusRecurringRunner = null; } + if (mTvInputManagerHelper != null) { + mTvInputManagerHelper.removeCallback(mTvInputCallback); + } super.onDestroy(); } @@ -2473,7 +2454,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC public void done(boolean success) { if (success) { mLastAllowedRatingForCurrentChannel = rating; - mTvView.requestUnblockContent(rating); + mTvView.unblockContent(rating); } } }); @@ -2500,7 +2481,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW); } if (keyCode != KeyEvent.KEYCODE_E) { - mOverlayManager.showMenu(Menu.REASON_NONE); + mOverlayManager.showMenu(mTvView.isRecordingPlayback() + ? Menu.REASON_RECORDING_PLAYBACK : Menu.REASON_NONE); } return true; case KeyEvent.KEYCODE_CHANNEL_UP: @@ -2603,17 +2585,18 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC case KeyEvent.KEYCODE_PROG_YELLOW: case KeyEvent.KEYCODE_BUTTON_Y: case KeyEvent.KEYCODE_Y: { - if (CommonFeatures.DVR.isEnabled(this)) { + if (CommonFeatures.DVR.isEnabled(this) && BuildCompat.isAtLeastN()) { // TODO(DVR) only get finished recordings. - List<Recording> recordings = mDvrDataManager.getRecordings(); - Log.d(TAG, "Found " + recordings.size() + " recordings"); - if (recordings.isEmpty()) { + List<RecordedProgram> recordedPrograms = mDvrDataManager + .getRecordedPrograms(); + Log.d(TAG, "Found " + recordedPrograms.size() + " recordings"); + if (recordedPrograms.isEmpty()) { Toast.makeText(this, "No finished recording to play", Toast.LENGTH_LONG) .show(); } else { - Recording r = recordings.get(0); + RecordedProgram r = recordedPrograms.get(0); Intent intent = new Intent(this, DvrPlayActivity.class); - intent.putExtra(Recording.RECORDING_ID_EXTRA, r.getId()); + intent.putExtra(ScheduledRecording.RECORDING_ID_EXTRA, r.getId()); startActivity(intent); } return true; @@ -2664,6 +2647,20 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } + @Override + public void enterPictureInPictureMode() { + // We need to hide overlay first, before moving the activity to PIP. If not, UI will + // be shown during PIP stack resizing, because UI and its animation is stuck during + // PIP resizing. + mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION); + mHandler.post(new Runnable() { + @Override + public void run() { + MainActivity.super.enterPictureInPictureMode(); + } + }); + } + public void togglePipView() { enablePipView(!mPipEnabled, true); mOverlayManager.getMenu().update(); @@ -2714,7 +2711,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC // Recover the stream volume of the main TV view, if needed. if (mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW) { setVolumeByAudioFocusStatus(mTvView); - mPipView.setStreamVolume(AUDIO_MIN_VOLUME); mPipSound = TvSettings.PIP_SOUND_MAIN; mTvOptionsManager.onPipSoundChanged(mPipSound); } @@ -2877,10 +2873,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } if (mPipSound == TvSettings.PIP_SOUND_MAIN) { setVolumeByAudioFocusStatus(mTvView); - mPipView.setStreamVolume(AUDIO_MIN_VOLUME); } else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW setVolumeByAudioFocusStatus(mPipView); - mTvView.setStreamVolume(AUDIO_MIN_VOLUME); } mPipSwap = !mPipSwap; mTvOptionsManager.onPipSwapChanged(mPipSwap); @@ -2896,11 +2890,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } if (mPipSound == TvSettings.PIP_SOUND_MAIN) { setVolumeByAudioFocusStatus(mPipView); - mTvView.setStreamVolume(AUDIO_MIN_VOLUME); mPipSound = TvSettings.PIP_SOUND_PIP_WINDOW; } else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW setVolumeByAudioFocusStatus(mTvView); - mPipView.setStreamVolume(AUDIO_MIN_VOLUME); mPipSound = TvSettings.PIP_SOUND_MAIN; } restoreMainTvView(); @@ -2929,9 +2921,26 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } stopPip(); mVisibleBehind = false; + if (!mOtherActivityLaunched && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) { + // Workaround: in M, onStop is not called, even though it should be called after + // onVisibleBehindCanceled is called. As a workaround, we call finish(). + finish(); + } super.onVisibleBehindCanceled(); } + @Override + public void startActivity(Intent intent) { + mOtherActivityLaunched = true; + super.startActivity(intent); + } + + @Override + public void startActivityForResult(Intent intent, int requestCode) { + mOtherActivityLaunched = true; + super.startActivityForResult(intent, requestCode); + } + public List<TvTrackInfo> getTracks(int type) { return mTvView.getTracks(type); } @@ -3015,10 +3024,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING: case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING: case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY: - return; case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: - stringId = R.string.msg_channel_unavailable_weak_signal; - break; + return; case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: default: stringId = R.string.msg_channel_unavailable_unknown; @@ -3119,4 +3126,123 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return CHANNEL_CHANGE_DELAY_MS_IN_NORMAL_SPEED; } } + + private class MyOnTuneListener implements OnTuneListener { + boolean mUnlockAllowedRatingBeforeShrunken = true; + boolean mWasUnderShrunkenTvView; + long mStreamInfoUpdateTimeThresholdMs; + Channel mChannel; + + public MyOnTuneListener() { } + + private void onTune(Channel channel, boolean wasUnderShrukenTvView) { + mStreamInfoUpdateTimeThresholdMs = + System.currentTimeMillis() + FIRST_STREAM_INFO_UPDATE_DELAY_MILLIS; + mChannel = channel; + mWasUnderShrunkenTvView = wasUnderShrukenTvView; + } + + private void onPlayRecording() { + mStreamInfoUpdateTimeThresholdMs = + System.currentTimeMillis() + FIRST_STREAM_INFO_UPDATE_DELAY_MILLIS; + mChannel = null; + mWasUnderShrunkenTvView = false; + } + + @Override + public void onUnexpectedStop(Channel channel) { + stopTv(); + startTv(null); + } + + @Override + public void onTuneFailed(Channel channel) { + Log.w(TAG, "Failed to tune to channel " + channel.getId() + + "@" + channel.getInputId()); + if (mTvView.isFadedOut()) { + mTvView.removeFadeEffect(); + } + // TODO: show something to user about this error. + } + + @Override + public void onStreamInfoChanged(StreamInfo info) { + if (info.isVideoAvailable() && mTuneDurationTimer.isRunning()) { + mTracker.sendChannelTuneTime(info.getCurrentChannel(), + mTuneDurationTimer.reset()); + } + // If updateChannelBanner() is called without delay, the stream info seems flickering + // when the channel is quickly changed. + if (!mHandler.hasMessages(MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE) + && info.isVideoAvailable()) { + if (System.currentTimeMillis() > mStreamInfoUpdateTimeThresholdMs) { + updateChannelBannerAndShowIfNeeded( + UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO); + } else { + mHandler.sendMessageDelayed(mHandler.obtainMessage( + MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE), + mStreamInfoUpdateTimeThresholdMs - System.currentTimeMillis()); + } + } + + applyDisplayRefreshRate(info.getVideoFrameRate()); + mTvViewUiManager.updateTvView(); + applyMultiAudio(); + applyClosedCaption(); + // TODO: Send command to TIS with checking the settings in TV and CaptionManager. + mOverlayManager.getMenu().onStreamInfoChanged(); + if (mTvView.isVideoAvailable()) { + mTvViewUiManager.fadeInTvView(); + } + mHandler.removeCallbacks(mRestoreMainViewRunnable); + restoreMainTvView(); + } + + @Override + public void onChannelRetuned(Uri channel) { + if (channel == null) { + return; + } + Channel currentChannel = + mChannelDataManager.getChannel(ContentUris.parseId(channel)); + if (currentChannel == null) { + Log.e(TAG, "onChannelRetuned is called but can't find a channel with the URI " + + channel); + return; + } + if (isChannelChangeKeyDownReceived()) { + // Ignore this message if the user is changing the channel. + return; + } + mChannelTuner.setCurrentChannel(currentChannel); + mTvView.setCurrentChannel(currentChannel); + updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE); + } + + @Override + public void onContentBlocked() { + mTuneDurationTimer.reset(); + TvContentRating rating = mTvView.getBlockedContentRating(); + // When tuneTo was called while TV view was shrunken, if the channel id is the same + // with the channel watched before shrunken, we allow the rating which was allowed + // before. + if (mWasUnderShrunkenTvView && mUnlockAllowedRatingBeforeShrunken + && mChannelBeforeShrunkenTvView.equals(mChannel) + && rating.equals(mAllowedRatingBeforeShrunken)) { + mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView(); + mTvView.unblockContent(rating); + } + + updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); + mTvViewUiManager.fadeInTvView(); + } + + @Override + public void onContentAllowed() { + if (!isUnderShrunkenTvView()) { + mUnlockAllowedRatingBeforeShrunken = false; + } + updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); + } + } } diff --git a/src/com/android/tv/MainActivityWrapper.java b/src/com/android/tv/MainActivityWrapper.java index 94f11864..82e96d14 100644 --- a/src/com/android/tv/MainActivityWrapper.java +++ b/src/com/android/tv/MainActivityWrapper.java @@ -20,8 +20,8 @@ import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; +import android.util.ArraySet; -import com.android.tv.common.CollectionUtils; import com.android.tv.data.Channel; import java.util.Set; @@ -34,7 +34,7 @@ import java.util.Set; public final class MainActivityWrapper { private MainActivity mActivity; - private final Set<OnCurrentChannelChangeListener> mListeners = CollectionUtils.createSmallSet(); + private final Set<OnCurrentChannelChangeListener> mListeners = new ArraySet<>(); /** * Returns the current main activity. diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java index bdabf25b..e6373505 100644 --- a/src/com/android/tv/SetupPassthroughActivity.java +++ b/src/com/android/tv/SetupPassthroughActivity.java @@ -23,9 +23,9 @@ import android.media.tv.TvInputInfo; import android.os.Bundle; import android.util.Log; +import com.android.tv.common.SoftPreconditions; import com.android.tv.common.TvCommonConstants; import com.android.tv.util.SetupUtils; -import com.android.tv.util.SoftPreconditions; import com.android.tv.util.TvInputManagerHelper; /** diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java index f96464e3..a231c29d 100644 --- a/src/com/android/tv/TimeShiftManager.java +++ b/src/com/android/tv/TimeShiftManager.java @@ -28,7 +28,9 @@ import android.util.Log; import android.util.Range; import com.android.tv.analytics.Tracker; +import com.android.tv.common.SoftPreconditions; import com.android.tv.common.WeakHandler; +import com.android.tv.common.recording.RecordedProgram; import com.android.tv.data.Channel; import com.android.tv.data.OnCurrentProgramUpdatedListener; import com.android.tv.data.Program; @@ -179,7 +181,7 @@ public class TimeShiftManager { tvView.setOnScreenBlockedListener(new TunableTvView.OnScreenBlockingChangedListener() { @Override public void onScreenBlockingChanged(boolean blocked) { - onAvailabilityChanged(); + mPlayController.onAvailabilityChanged(); } }); } @@ -195,7 +197,7 @@ public class TimeShiftManager { * Checks if the trick play is available for the current channel. */ public boolean isAvailable() { - return mPlayController.isAvailable(); + return mPlayController.mAvailable; } /** @@ -229,11 +231,6 @@ public class TimeShiftManager { } } - public boolean isPlayForRecording() { - // TODO: need to find better way to check if it's for recording playback. - return mPlayController.mRecordEndTimeMs != CURRENT_TIME; - } - /** * Plays the media. * @@ -470,11 +467,18 @@ public class TimeShiftManager { } /** + * Checks whether the TV is playing the recorded content. + */ + public boolean isRecordingPlayback() { + return mPlayController.mRecordingPlayback; + } + + /** * Returns {@code true} if the trick play is available and it's playing to the forward direction * with normal speed, otherwise {@code false}. */ public boolean isNormalPlaying() { - return mPlayController.isAvailable() + return mPlayController.mAvailable && mPlayController.mPlayStatus == PLAY_STATUS_PLAYING && mPlayController.mPlayDirection == PLAY_DIRECTION_FORWARD && mPlayController.mDisplayedPlaySpeed == PLAY_SPEED_1X; @@ -484,7 +488,7 @@ public class TimeShiftManager { * Checks if the trick play is available and it's playback status is paused. */ public boolean isPaused() { - return mPlayController.isAvailable() && mPlayController.mPlayStatus == PLAY_STATUS_PAUSED; + return mPlayController.mAvailable && mPlayController.mPlayStatus == PLAY_STATUS_PAUSED; } /** @@ -502,8 +506,9 @@ public class TimeShiftManager { } void onAvailabilityChanged() { - mProgramManager.onAvailabilityChanged(mPlayController.isAvailable(), - mPlayController.getCurrentChannel(), mPlayController.mRecordStartTimeMs); + mProgramManager.onAvailabilityChanged(mPlayController.mAvailable, + mPlayController.mRecordingPlayback ? null : mPlayController.getCurrentChannel(), + mPlayController.mRecordStartTimeMs); updateActions(); // Availability change notification should be always sent // even if mNotificationEnabled is false. @@ -513,7 +518,7 @@ public class TimeShiftManager { } void onRecordTimeRangeChanged() { - if (mPlayController.isAvailable()) { + if (mPlayController.mAvailable) { mProgramManager.onRecordTimeRangeChanged(mPlayController.mRecordStartTimeMs, mPlayController.mRecordEndTimeMs); } @@ -590,7 +595,6 @@ public class TimeShiftManager { private class PlayController { private final TunableTvView mTvView; - private long mPossibleStartTimeMs; private long mRecordStartTimeMs; private long mRecordEndTimeMs; @@ -598,6 +602,8 @@ public class TimeShiftManager { @PlaySpeed private int mDisplayedPlaySpeed = PLAY_SPEED_1X; @PlayDirection private int mPlayDirection = PLAY_DIRECTION_FORWARD; private int mPlaybackSpeed; + private boolean mAvailable; + private boolean mRecordingPlayback; /** * Indicates that the trick play is not playing the current time position. @@ -613,47 +619,11 @@ public class TimeShiftManager { mTvView.setTimeShiftListener(new TimeShiftListener() { @Override public void onAvailabilityChanged() { - // Do not send the notifications while the availability is changing, - // because the variables are in the intermediate state. - // For example, the current program can be null. - mNotificationEnabled = false; - mDisplayedPlaySpeed = PLAY_SPEED_1X; - mPlaybackSpeed = 1; - mPlayDirection = PLAY_DIRECTION_FORWARD; - mIsPlayOffsetChanged = false; - mPossibleStartTimeMs = System.currentTimeMillis(); - mRecordStartTimeMs = mPossibleStartTimeMs; - mRecordEndTimeMs = CURRENT_TIME; - mCurrentPositionMediator.initialize(mPossibleStartTimeMs); - mHandler.removeMessages(MSG_GET_CURRENT_POSITION); - - if (isAvailable()) { - // When the media availability message has come. - mPlayController.setPlayStatus(PLAY_STATUS_PLAYING); - mHandler.sendEmptyMessageDelayed(MSG_GET_CURRENT_POSITION, - REQUEST_CURRENT_POSITION_INTERVAL); - } else { - // When the tune command is sent. - mPlayController.setPlayStatus(PLAY_STATUS_PAUSED); - } - TimeShiftManager.this.onAvailabilityChanged(); - mNotificationEnabled = true; + PlayController.this.onAvailabilityChanged(); } @Override public void onRecordStartTimeChanged(long recordStartTimeMs) { - if (mRecordEndTimeMs == CURRENT_TIME && - recordStartTimeMs < mPossibleStartTimeMs) { - // Do not warn in this case because it can happen in normal cases. - if (DEBUG) { - Log.d(TAG, "Record start time is less then the time when it became " - + "available. {availableStartTime=" - + Utils.toTimeString(mPossibleStartTimeMs) - + ", recordStartTimeMs=" + Utils.toTimeString(recordStartTimeMs) - + "}"); - } - recordStartTimeMs = mPossibleStartTimeMs; - } if (mRecordStartTimeMs == recordStartTimeMs) { return; } @@ -675,26 +645,48 @@ public class TimeShiftManager { TimeShiftManager.this.play(); } } - - @Override - public void onRecordEndTimeChanged(long recordEndTimeMs) { - if (mRecordEndTimeMs == recordEndTimeMs) { - return; - } - mRecordEndTimeMs = recordEndTimeMs; - TimeShiftManager.this.onRecordTimeRangeChanged(); - - if (mPlayStatus == PLAY_STATUS_PLAYING && - mRecordEndTimeMs - getCurrentPositionMs() - < RECORDING_BOUNDARY_THRESHOLD) { - TimeShiftManager.this.pause(); - } - } }); } - boolean isAvailable() { - return mTvView.isTimeShiftAvailable() && !mTvView.isScreenBlocked(); + void onAvailabilityChanged() { + boolean newAvailable = mTvView.isTimeShiftAvailable() && !mTvView.isScreenBlocked(); + if (mAvailable == newAvailable) { + return; + } + mAvailable = newAvailable; + // Do not send the notifications while the availability is changing, + // because the variables are in the intermediate state. + // For example, the current program can be null. + mNotificationEnabled = false; + mDisplayedPlaySpeed = PLAY_SPEED_1X; + mPlaybackSpeed = 1; + mPlayDirection = PLAY_DIRECTION_FORWARD; + mRecordingPlayback = mTvView.isRecordingPlayback(); + if (mRecordingPlayback) { + RecordedProgram recordedProgram = mTvView.getPlayingRecordedProgram(); + SoftPreconditions.checkNotNull(recordedProgram); + mIsPlayOffsetChanged = true; + mRecordStartTimeMs = 0; + mRecordEndTimeMs = recordedProgram.getDurationMillis(); + } else { + mIsPlayOffsetChanged = false; + mRecordStartTimeMs = System.currentTimeMillis(); + mRecordEndTimeMs = CURRENT_TIME; + } + mCurrentPositionMediator.initialize(mRecordStartTimeMs); + mHandler.removeMessages(MSG_GET_CURRENT_POSITION); + + if (mAvailable) { + // When the media availability message has come. + mPlayController.setPlayStatus(PLAY_STATUS_PLAYING); + mHandler.sendEmptyMessageDelayed(MSG_GET_CURRENT_POSITION, + REQUEST_CURRENT_POSITION_INTERVAL); + } else { + // When the tune command is sent. + mPlayController.setPlayStatus(PLAY_STATUS_PAUSED); + } + TimeShiftManager.this.onAvailabilityChanged(); + mNotificationEnabled = true; } void handleGetCurrentPosition() { @@ -855,18 +847,25 @@ public class TimeShiftManager { private final List<Program> mPrograms = new ArrayList<>(); private final Queue<Range<Long>> mProgramLoadQueue = new LinkedList<>(); private LoadProgramsForCurrentChannelTask mProgramLoadTask = null; + private int mEmptyFetchCount = 0; ProgramManager(ProgramDataManager programDataManager) { mProgramDataManager = programDataManager; } void onAvailabilityChanged(boolean available, Channel channel, long currentPositionMs) { + if (DEBUG) { + Log.d(TAG, "onAvailabilityChanged(" + available + "+," + channel + ", " + + currentPositionMs + ")"); + } + mProgramLoadQueue.clear(); if (mProgramLoadTask != null) { mProgramLoadTask.cancel(true); } mHandler.removeMessages(MSG_PREFETCH_PROGRAM); mPrograms.clear(); + mEmptyFetchCount = 0; mChannel = channel; if (channel == null || channel.isPassthrough()) { return; @@ -1133,17 +1132,37 @@ public class TimeShiftManager { } Program lastValidProgram = getLastValidProgram(); if (DEBUG) Log.d(TAG, "Last valid program = " + lastValidProgram); + final long delay; if (lastValidProgram != null) { - long delay = lastValidProgram.getEndTimeUtcMillis() + delay = lastValidProgram.getEndTimeUtcMillis() - PREFETCH_TIME_OFFSET_FROM_PROGRAM_END - System.currentTimeMillis(); - mHandler.sendEmptyMessageDelayed(MSG_PREFETCH_PROGRAM, delay); - if (DEBUG) Log.d(TAG, "Scheduling with " + delay + "(ms) delays."); } else { - mHandler.sendEmptyMessage(MSG_PREFETCH_PROGRAM); - if (DEBUG) Log.d(TAG, "Scheduling promptly."); + // Since there might not be any program data delay the retry 5 seconds, + // then 30 seconds then 5 minutes + switch (mEmptyFetchCount) { + case 0: + delay = 0; + break; + case 1: + delay = TimeUnit.SECONDS.toMillis(5); + break; + case 2: + delay = TimeUnit.SECONDS.toMillis(30); + break; + default: + delay = TimeUnit.MINUTES.toMillis(5); + break; + } + if (DEBUG) { + Log.d(TAG, + "No last valid program. Already tried " + mEmptyFetchCount + " times"); + } } + mHandler.sendEmptyMessageDelayed(MSG_PREFETCH_PROGRAM, delay); + if (DEBUG) Log.d(TAG, "Scheduling with " + delay + "(ms) delays."); } + // Prefecth programs within PREFETCH_DURATION_FOR_NEXT from now. private void prefetchPrograms() { long startTimeMs; Program lastValidProgram = getLastValidProgram(); @@ -1153,11 +1172,13 @@ public class TimeShiftManager { startTimeMs = lastValidProgram.getEndTimeUtcMillis(); } long endTimeMs = System.currentTimeMillis() + PREFETCH_DURATION_FOR_NEXT; - if (DEBUG) { - Log.d(TAG, "Prefetch task starts: {startTime=" + Utils.toTimeString(startTimeMs) - + ", endTime=" + Utils.toTimeString(endTimeMs) + "}"); + if (startTimeMs <= endTimeMs) { + if (DEBUG) { + Log.d(TAG, "Prefetch task starts: {startTime=" + Utils.toTimeString(startTimeMs) + + ", endTime=" + Utils.toTimeString(endTimeMs) + "}"); + } + mProgramLoadQueue.add(Range.create(startTimeMs, endTimeMs)); } - mProgramLoadQueue.add(Range.create(startTimeMs, endTimeMs)); startTaskIfNeeded(); } @@ -1185,8 +1206,8 @@ public class TimeShiftManager { it.remove(); } } - if (programs == null || programs.isEmpty() || mPrograms.isEmpty()) { - mPrograms.addAll(programs); + if (programs == null || programs.isEmpty()) { + mEmptyFetchCount++; if (addDummyPrograms(mPeriod)) { TimeShiftManager.this.onProgramInfoChanged(); } @@ -1194,19 +1215,22 @@ public class TimeShiftManager { startNextLoadingIfNeeded(); return; } - removeDummyPrograms(); - removeOverlappedPrograms(programs); - Program loadedProgram = programs.get(0); - for (int i = 0; i < mPrograms.size() && !programs.isEmpty(); ++i) { - Program program = mPrograms.get(i); - while (program.getStartTimeUtcMillis() > loadedProgram - .getStartTimeUtcMillis()) { - mPrograms.add(i++, loadedProgram); - programs.remove(0); - if (programs.isEmpty()) { - break; + mEmptyFetchCount = 0; + if(!mPrograms.isEmpty()) { + removeDummyPrograms(); + removeOverlappedPrograms(programs); + Program loadedProgram = programs.get(0); + for (int i = 0; i < mPrograms.size() && !programs.isEmpty(); ++i) { + Program program = mPrograms.get(i); + while (program.getStartTimeUtcMillis() > loadedProgram + .getStartTimeUtcMillis()) { + mPrograms.add(i++, loadedProgram); + programs.remove(0); + if (programs.isEmpty()) { + break; + } + loadedProgram = programs.get(0); } - loadedProgram = programs.get(0); } } mPrograms.addAll(programs); diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java index 0cac4a3b..ef105c94 100644 --- a/src/com/android/tv/TvApplication.java +++ b/src/com/android/tv/TvApplication.java @@ -16,6 +16,7 @@ package com.android.tv; +import android.annotation.TargetApi; import android.app.Activity; import android.app.Application; import android.content.ComponentName; @@ -23,14 +24,15 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.media.tv.TvContract; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvInputManager.TvInputCallback; +import android.os.Build; import android.os.Bundle; import android.os.StrictMode; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.annotation.UiThread; +import android.support.v4.os.BuildCompat; import android.util.Log; import android.view.KeyEvent; @@ -47,14 +49,17 @@ import com.android.tv.data.ChannelDataManager; import com.android.tv.data.ProgramDataManager; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManagerImpl; -import com.android.tv.dvr.DvrDataManagerInMemoryImpl; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrRecordingService; import com.android.tv.dvr.DvrSessionManager; +import com.android.tv.util.Clock; import com.android.tv.util.SetupUtils; import com.android.tv.util.SystemProperties; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; +import com.android.usbtuner.UsbTunerPreferences; +import com.android.usbtuner.setup.TunerSetupActivity; +import com.android.usbtuner.tvinput.UsbTunerTvInputService; import java.util.List; @@ -127,7 +132,7 @@ public class TvApplication extends Application implements ApplicationSingletons handleInputCountChanged(); } }); - if (CommonFeatures.DVR.isEnabled(this)) { + if (CommonFeatures.DVR.isEnabled(this) && BuildCompat.isAtLeastN()) { mDvrManager = new DvrManager(this); //NOTE: DvrRecordingService just keeps running. DvrRecordingService.startService(this); @@ -148,6 +153,7 @@ public class TvApplication extends Application implements ApplicationSingletons } @Override + @TargetApi(Build.VERSION_CODES.N) public DvrSessionManager getDvrSessionManger() { if (mDvrSessionManager == null) { mDvrSessionManager = new DvrSessionManager(this); @@ -199,16 +205,13 @@ public class TvApplication extends Application implements ApplicationSingletons /** * Returns {@link DvrDataManager}. */ + @TargetApi(Build.VERSION_CODES.N) @Override public DvrDataManager getDvrDataManager() { if (mDvrDataManager == null) { - if(SystemProperties.USE_IN_MEMORY_DVR_DB.getValue()){ - mDvrDataManager = new DvrDataManagerInMemoryImpl(this); - } else { - DvrDataManagerImpl dvrDataManager = new DvrDataManagerImpl(this); + DvrDataManagerImpl dvrDataManager = new DvrDataManagerImpl(this, Clock.SYSTEM); mDvrDataManager = dvrDataManager; dvrDataManager.start(); - } } return mDvrDataManager; } @@ -256,7 +259,9 @@ public class TvApplication extends Application implements ApplicationSingletons boolean hasTunerInput = false; for (TvInputInfo input : tvInputs) { if (input.isPassthroughInput()) { - ++inputCount; + if (!input.isHidden(this)) { + ++inputCount; + } } else if (!hasTunerInput) { hasTunerInput = true; ++inputCount; @@ -310,17 +315,38 @@ public class TvApplication extends Application implements ApplicationSingletons * {@link SetupUtils}. */ public void handleInputCountChanged() { + handleInputCountChanged(false, false, false); + } + + /** + * Checks the input counts and enable/disable TvActivity. Also updates the input list in + * {@link SetupUtils}. + * + * @param calledByTunerServiceChanged true if it is called when UsbTunerTvInputService + * is enabled or disabled. + * @param tunerServiceEnabled it's available only when calledByTunerServiceChanged is true. + * @param dontKillApp when TvActivity is enabled or disabled by this method, the app restarts + * by default. But, if dontKillApp is true, the app won't restart. + */ + public void handleInputCountChanged(boolean calledByTunerServiceChanged, + boolean tunerServiceEnabled, boolean dontKillApp) { TvInputManager inputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE); - boolean enable = false; - if (Features.UNHIDE.isEnabled(TvApplication.this)) { - enable = true; - } else { + boolean enable = (calledByTunerServiceChanged && tunerServiceEnabled) + || Features.UNHIDE.isEnabled(TvApplication.this); + if (!enable) { List<TvInputInfo> inputs = inputManager.getTvInputList(); + boolean skipTunerInputCheck = false; // Enable the TvActivity only if there is at least one tuner type input. - for (TvInputInfo input : inputs) { - if (input.getType() == TvInputInfo.TYPE_TUNER) { - enable = true; - break; + if (!skipTunerInputCheck) { + for (TvInputInfo input : inputs) { + if (calledByTunerServiceChanged && !tunerServiceEnabled + && UsbTunerTvInputService.getInputId(this).equals(input.getId())) { + continue; + } + if (input.getType() == TvInputInfo.TYPE_TUNER) { + enable = true; + break; + } } } if (DEBUG) Log.d(TAG, "Enable MainActivity: " + enable); @@ -330,7 +356,8 @@ public class TvApplication extends Application implements ApplicationSingletons int newState = enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; if (packageManager.getComponentEnabledSetting(name) != newState) { - packageManager.setComponentEnabledSetting(name, newState, 0); + packageManager.setComponentEnabledSetting(name, newState, + dontKillApp ? PackageManager.DONT_KILL_APP : 0); } SetupUtils.getInstance(TvApplication.this).onInputListUpdated(inputManager); } diff --git a/src/com/android/tv/TvOptionsManager.java b/src/com/android/tv/TvOptionsManager.java index 97b9d5fa..f104e75d 100644 --- a/src/com/android/tv/TvOptionsManager.java +++ b/src/com/android/tv/TvOptionsManager.java @@ -35,10 +35,11 @@ import java.util.Locale; public class TvOptionsManager { public static final int OPTION_CLOSED_CAPTIONS = 0; public static final int OPTION_DISPLAY_MODE = 1; - public static final int OPTION_PIP = 2; - public static final int OPTION_MULTI_AUDIO = 3; - public static final int OPTION_MORE_CHANNELS = 4; - public static final int OPTION_SETTINGS = 5; + public static final int OPTION_IN_APP_PIP = 2; + public static final int OPTION_SYSTEMWIDE_PIP = 3; + public static final int OPTION_MULTI_AUDIO = 4; + public static final int OPTION_MORE_CHANNELS = 5; + public static final int OPTION_SETTINGS = 6; public static final int OPTION_PIP_INPUT = 100; public static final int OPTION_PIP_SWAP = 101; @@ -75,7 +76,7 @@ public class TvOptionsManager { .isDisplayModeAvailable(mDisplayMode) ? DisplayMode.getLabel(mDisplayMode, mContext) : DisplayMode.getLabel(DisplayMode.MODE_NORMAL, mContext); - case OPTION_PIP: + case OPTION_IN_APP_PIP: return mContext.getString( mPip ? R.string.options_item_pip_on : R.string.options_item_pip_off); case OPTION_MULTI_AUDIO: @@ -130,7 +131,7 @@ public class TvOptionsManager { public void onPipChanged(boolean pip) { mPip = pip; - notifyOptionChanged(OPTION_PIP); + notifyOptionChanged(OPTION_IN_APP_PIP); } public void onMultiAudioChanged(String multiAudio) { diff --git a/src/com/android/tv/analytics/SendConfigInfoRunnable.java b/src/com/android/tv/analytics/SendConfigInfoRunnable.java index c2d5c5fb..41392a6d 100644 --- a/src/com/android/tv/analytics/SendConfigInfoRunnable.java +++ b/src/com/android/tv/analytics/SendConfigInfoRunnable.java @@ -26,8 +26,8 @@ import java.util.List; * Sends ConfigurationInfo once a day. */ public class SendConfigInfoRunnable implements Runnable { - private Tracker mTracker; - private TvInputManagerHelper mTvInputManagerHelper; + private final Tracker mTracker; + private final TvInputManagerHelper mTvInputManagerHelper; public SendConfigInfoRunnable(Tracker tracker, TvInputManagerHelper tvInputManagerHelper) { this.mTracker = tracker; diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java index ba3c59ba..86437ab2 100644 --- a/src/com/android/tv/data/Channel.java +++ b/src/com/android/tv/data/Channel.java @@ -33,7 +33,6 @@ import android.util.Log; import com.android.tv.common.CollectionUtils; import com.android.tv.common.TvCommonConstants; -import com.android.tv.dvr.provider.DvrContract; import com.android.tv.util.ImageLoader; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -75,17 +74,17 @@ public final class Channel { private static final String INVALID_PACKAGE_NAME = "packageName"; private static final String[] PROJECTION_BASE = { - // Columns must match what is read in Channel.fromCursor() - TvContract.Channels._ID, - TvContract.Channels.COLUMN_PACKAGE_NAME, - TvContract.Channels.COLUMN_INPUT_ID, - TvContract.Channels.COLUMN_TYPE, - TvContract.Channels.COLUMN_DISPLAY_NUMBER, - TvContract.Channels.COLUMN_DISPLAY_NAME, - TvContract.Channels.COLUMN_DESCRIPTION, - TvContract.Channels.COLUMN_VIDEO_FORMAT, - TvContract.Channels.COLUMN_BROWSABLE, - TvContract.Channels.COLUMN_LOCKED, + // Columns must match what is read in Channel.fromCursor() + TvContract.Channels._ID, + TvContract.Channels.COLUMN_PACKAGE_NAME, + TvContract.Channels.COLUMN_INPUT_ID, + TvContract.Channels.COLUMN_TYPE, + TvContract.Channels.COLUMN_DISPLAY_NUMBER, + TvContract.Channels.COLUMN_DISPLAY_NAME, + TvContract.Channels.COLUMN_DESCRIPTION, + TvContract.Channels.COLUMN_VIDEO_FORMAT, + TvContract.Channels.COLUMN_BROWSABLE, + TvContract.Channels.COLUMN_LOCKED, }; // Additional fields added in MNC. @@ -110,15 +109,6 @@ public final class Channel { } /** - * Use this projection if you want to create {@link Channel} object using - * {@link #fromDvrCursor}. - */ - public static final String[] PROJECTION_DVR = { - // Columns must match what is read in Channel.fromDvrCursor() - DvrContract.DvrChannels._ID - }; - - /** * Creates {@code Channel} object from cursor. * * <p>The query that created the cursor MUST use {@link #PROJECTION} @@ -264,6 +254,13 @@ public final class Channel { } /** + * Checks whether this channel is physical tuner channel or not. + */ + public boolean isPhysicalTunerChannel() { + return !TextUtils.isEmpty(mType) && !TvContract.Channels.TYPE_OTHER.equals(mType); + } + + /** * Checks if two channels equal by checking ids. */ @Override diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java index 82ac4b5a..84a16111 100644 --- a/src/com/android/tv/data/ChannelDataManager.java +++ b/src/com/android/tv/data/ChannelDataManager.java @@ -31,15 +31,15 @@ import android.os.Message; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; +import android.util.ArraySet; import android.util.Log; import android.util.MutableInt; -import com.android.tv.common.CollectionUtils; import com.android.tv.common.SharedPreferencesUtils; +import com.android.tv.common.SoftPreconditions; import com.android.tv.common.WeakHandler; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.PermissionUtils; -import com.android.tv.util.SoftPreconditions; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -72,7 +72,7 @@ public class ChannelDataManager { private QueryAllChannelsTask mChannelsUpdateTask; private final List<Runnable> mPostRunnablesAfterChannelUpdate = new ArrayList<>(); - private final Set<Listener> mListeners = CollectionUtils.createSmallSet(); + private final Set<Listener> mListeners = new ArraySet<>(); private final Map<Long, ChannelWrapper> mChannelWrapperMap = new HashMap<>(); private final Map<String, MutableInt> mChannelCountMap = new HashMap<>(); private final Channel.DefaultComparator mChannelComparator; @@ -282,7 +282,7 @@ public class ChannelDataManager { channels.add(channel); } } - return Collections.unmodifiableList(channels); + return channels; } /** @@ -508,6 +508,15 @@ public class ChannelDataManager { mChannelsUpdateTask.executeOnDbThread(); } + /** + * Reloads channel data. + */ + public void reload() { + if (mDbLoadFinished && !mHandler.hasMessages(MSG_UPDATE_CHANNELS)) { + mHandler.sendEmptyMessage(MSG_UPDATE_CHANNELS); + } + } + public interface Listener { /** * Called when data load is finished. @@ -539,7 +548,7 @@ public class ChannelDataManager { } private class ChannelWrapper { - final Set<ChannelListener> mChannelListeners = CollectionUtils.createSmallSet(); + final Set<ChannelListener> mChannelListeners = new ArraySet<>(); final Channel mChannel; boolean mBrowsableInDb; boolean mLockedInDb; diff --git a/src/com/android/tv/data/GenreItems.java b/src/com/android/tv/data/GenreItems.java index 92e38809..b1110612 100644 --- a/src/com/android/tv/data/GenreItems.java +++ b/src/com/android/tv/data/GenreItems.java @@ -17,13 +17,11 @@ package com.android.tv.data; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.content.Context; import android.media.tv.TvContract.Programs.Genres; import android.os.Build; import com.android.tv.R; -import com.android.tv.common.CollectionUtils; public class GenreItems { /** @@ -31,7 +29,7 @@ public class GenreItems { */ public static final int ID_ALL_CHANNELS = 0; - private static final String[] CANONICAL_GENRES_BASE = { + private static final String[] CANONICAL_GENRES_L = { null, // All channels Genres.FAMILY_KIDS, Genres.SPORTS, @@ -47,23 +45,34 @@ public class GenreItems { }; @SuppressLint("InlinedApi") - private static final String[] CANONICAL_GENRES_ADDED_IN_L_MR1 = { - Genres.ARTS, - Genres.ENTERTAINMENT, - Genres.LIFE_STYLE, - Genres.MUSIC, - Genres.PREMIER, - Genres.TECH_SCIENCE + private static final String[] CANONICAL_GENRES_L_MR1 = { + null, // All channels + Genres.FAMILY_KIDS, + Genres.SPORTS, + Genres.SHOPPING, + Genres.MOVIES, + Genres.COMEDY, + Genres.TRAVEL, + Genres.DRAMA, + Genres.EDUCATION, + Genres.ANIMAL_WILDLIFE, + Genres.NEWS, + Genres.GAMING, + Genres.ARTS, + Genres.ENTERTAINMENT, + Genres.LIFE_STYLE, + Genres.MUSIC, + Genres.PREMIER, + Genres.TECH_SCIENCE }; private static final String[] CANONICAL_GENRES = createGenres(); private static String[] createGenres() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) { - return CANONICAL_GENRES_BASE; + return CANONICAL_GENRES_L; } else { - return CollectionUtils - .concatAll(CANONICAL_GENRES_BASE, CANONICAL_GENRES_ADDED_IN_L_MR1); + return CANONICAL_GENRES_L_MR1; } } @@ -73,7 +82,9 @@ public class GenreItems { * Returns array of all genre labels. */ public static String[] getLabels(Context context) { - String[] items = context.getResources().getStringArray(R.array.genre_labels); + String[] items = Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1 + ? context.getResources().getStringArray(R.array.genre_labels_l) + : context.getResources().getStringArray(R.array.genre_labels_l_mr1); if (items.length != CANONICAL_GENRES.length) { throw new IllegalArgumentException("Genre data mismatch"); } diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/Program.java index b9c54aac..af5f93bb 100644 --- a/src/com/android/tv/data/Program.java +++ b/src/com/android/tv/data/Program.java @@ -22,13 +22,14 @@ import android.media.tv.TvContentRating; import android.media.tv.TvContract; import android.support.annotation.NonNull; import android.support.annotation.UiThread; +import android.support.v4.os.BuildCompat; import android.text.TextUtils; import android.util.Log; import com.android.tv.R; import com.android.tv.common.BuildConfig; +import com.android.tv.common.CollectionUtils; import com.android.tv.common.TvContentRatingCache; -import com.android.tv.dvr.provider.DvrContract; import com.android.tv.util.ImageLoader; import com.android.tv.util.Utils; @@ -43,33 +44,43 @@ public final class Program implements Comparable<Program> { private static final boolean DEBUG_DUMP_DESCRIPTION = false; private static final String TAG = "Program"; - public static final String[] PROJECTION = { - // Columns must match what is read in Program.fromCursor() - TvContract.Programs.COLUMN_CHANNEL_ID, - TvContract.Programs.COLUMN_TITLE, - TvContract.Programs.COLUMN_EPISODE_TITLE, - TvContract.Programs.COLUMN_SEASON_NUMBER, - TvContract.Programs.COLUMN_EPISODE_NUMBER, - TvContract.Programs.COLUMN_SHORT_DESCRIPTION, - TvContract.Programs.COLUMN_POSTER_ART_URI, - TvContract.Programs.COLUMN_THUMBNAIL_URI, - TvContract.Programs.COLUMN_CANONICAL_GENRE, - TvContract.Programs.COLUMN_CONTENT_RATING, - TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, - TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, - TvContract.Programs.COLUMN_VIDEO_WIDTH, - TvContract.Programs.COLUMN_VIDEO_HEIGHT + private static final String[] PROJECTION_BASE = { + // Columns must match what is read in Program.fromCursor() + TvContract.Programs._ID, + TvContract.Programs.COLUMN_CHANNEL_ID, + TvContract.Programs.COLUMN_TITLE, + TvContract.Programs.COLUMN_EPISODE_TITLE, + TvContract.Programs.COLUMN_SHORT_DESCRIPTION, + TvContract.Programs.COLUMN_POSTER_ART_URI, + TvContract.Programs.COLUMN_THUMBNAIL_URI, + TvContract.Programs.COLUMN_CANONICAL_GENRE, + TvContract.Programs.COLUMN_CONTENT_RATING, + TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, + TvContract.Programs.COLUMN_VIDEO_WIDTH, + TvContract.Programs.COLUMN_VIDEO_HEIGHT }; - /** - * Use this projection if you want to create {@link Program} object using - * {@link #fromDvrCursor}. - */ - public static final String[] PROJECTION_DVR = { - // Columns must match what is read in Channel.fromDvrCursor() - DvrContract.DvrPrograms._ID + // Columns which is deprecated in NYC + private static final String[] PROJECTION_DEPRECATED_IN_NYC = { + TvContract.Programs.COLUMN_SEASON_NUMBER, + TvContract.Programs.COLUMN_EPISODE_NUMBER + }; + + private static final String[] PROJECTION_ADDED_IN_NYC = { + TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, + TvContract.Programs.COLUMN_SEASON_TITLE, + TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER }; + public static final String[] PROJECTION = createProjection(); + + private static String[] createProjection() { + return CollectionUtils + .concatAll(PROJECTION_BASE, BuildCompat.isAtLeastN() ? PROJECTION_ADDED_IN_NYC + : PROJECTION_DEPRECATED_IN_NYC); + } + /** * Creates {@code Program} object from cursor. * @@ -79,39 +90,38 @@ public final class Program implements Comparable<Program> { // Columns read must match the order of match {@link #PROJECTION} Builder builder = new Builder(); int index = 0; + builder.setId(cursor.getLong(index++)); builder.setChannelId(cursor.getLong(index++)); builder.setTitle(cursor.getString(index++)); builder.setEpisodeTitle(cursor.getString(index++)); - builder.setSeasonNumber(cursor.getInt(index++)); - builder.setEpisodeNumber(cursor.getInt(index++)); builder.setDescription(cursor.getString(index++)); builder.setPosterArtUri(cursor.getString(index++)); builder.setThumbnailUri(cursor.getString(index++)); builder.setCanonicalGenres(cursor.getString(index++)); - builder.setContentRatings(TvContentRatingCache.getInstance() - .getRatings(cursor.getString(index++))); + builder.setContentRatings( + TvContentRatingCache.getInstance().getRatings(cursor.getString(index++))); builder.setStartTimeUtcMillis(cursor.getLong(index++)); builder.setEndTimeUtcMillis(cursor.getLong(index++)); builder.setVideoWidth((int) cursor.getLong(index++)); builder.setVideoHeight((int) cursor.getLong(index++)); + if (BuildCompat.isAtLeastN()) { + builder.setSeasonNumber(cursor.getString(index++)); + builder.setSeasonTitle(cursor.getString(index++)); + builder.setEpisodeNumber(cursor.getString(index++)); + } else { + builder.setSeasonNumber(cursor.getString(index++)); + builder.setEpisodeNumber(cursor.getString(index++)); + } return builder.build(); } - /** - * Creates a {@link Program} object from the DVR database. - */ - public static Program fromDvrCursor(Cursor c) { - Program program = new Program(); - int index = -1; - program.mDvrId = c.getLong(++index); - return program; - } - + private long mId; private long mChannelId; private String mTitle; private String mEpisodeTitle; - private int mSeasonNumber; - private int mEpisodeNumber; + private String mSeasonNumber; + private String mSeasonTitle; + private String mEpisodeNumber; private long mStartTimeUtcMillis; private long mEndTimeUtcMillis; private String mDescription; @@ -122,8 +132,6 @@ public final class Program implements Comparable<Program> { private int[] mCanonicalGenreIds; private TvContentRating[] mContentRatings; - private long mDvrId; - /** * TODO(DVR): Need to fill the following data. */ @@ -134,6 +142,10 @@ public final class Program implements Comparable<Program> { // Do nothing. } + public long getId() { + return mId; + } + public long getChannelId() { return mChannelId; } @@ -161,13 +173,22 @@ public final class Program implements Comparable<Program> { } public String getEpisodeDisplayTitle(Context context) { - if (mSeasonNumber > 0 && mEpisodeNumber > 0 && !TextUtils.isEmpty(mEpisodeTitle)) { + if (!TextUtils.isEmpty(mSeasonNumber) && !TextUtils.isEmpty(mEpisodeNumber) + && !TextUtils.isEmpty(mEpisodeTitle)) { return String.format(context.getResources().getString(R.string.episode_format), mSeasonNumber, mEpisodeNumber, mEpisodeTitle); } return mEpisodeTitle; } + public String getSeasonNumber() { + return mSeasonNumber; + } + + public String getEpisodeNumber() { + return mEpisodeNumber; + } + public long getStartTimeUtcMillis() { return mStartTimeUtcMillis; } @@ -239,19 +260,12 @@ public final class Program implements Comparable<Program> { return false; } - /** - * Returns an ID in DVR database. - */ - public long getDvrId() { - return mDvrId; - } - @Override public int hashCode() { return Objects.hash(mChannelId, mStartTimeUtcMillis, mEndTimeUtcMillis, mTitle, mEpisodeTitle, mDescription, mVideoWidth, mVideoHeight, mPosterArtUri, mThumbnailUri, Arrays.hashCode(mContentRatings), - Arrays.hashCode(mCanonicalGenreIds), mSeasonNumber, mEpisodeNumber); + Arrays.hashCode(mCanonicalGenreIds), mSeasonNumber, mSeasonTitle, mEpisodeNumber); } @Override @@ -272,8 +286,9 @@ public final class Program implements Comparable<Program> { && Objects.equals(mThumbnailUri, program.mThumbnailUri) && Arrays.equals(mContentRatings, program.mContentRatings) && Arrays.equals(mCanonicalGenreIds, program.mCanonicalGenreIds) - && mSeasonNumber == program.mSeasonNumber - && mEpisodeNumber == program.mEpisodeNumber; + && Objects.equals(mSeasonNumber, program.mSeasonNumber) + && Objects.equals(mSeasonTitle, program.mSeasonTitle) + && Objects.equals(mEpisodeNumber, program.mEpisodeNumber); } @Override @@ -284,11 +299,12 @@ public final class Program implements Comparable<Program> { @Override public String toString() { StringBuilder builder = new StringBuilder(); - builder.append("Program{") + builder.append("Program[" + mId + "]{") .append("channelId=").append(mChannelId) .append(", title=").append(mTitle) .append(", episodeTitle=").append(mEpisodeTitle) .append(", seasonNumber=").append(mSeasonNumber) + .append(", seasonTitle=").append(mSeasonTitle) .append(", episodeNumber=").append(mEpisodeNumber) .append(", startTimeUtcSec=").append(Utils.toTimeString(mStartTimeUtcMillis)) .append(", endTimeUtcSec=").append(Utils.toTimeString(mEndTimeUtcMillis)) @@ -310,10 +326,12 @@ public final class Program implements Comparable<Program> { return; } + mId = other.mId; mChannelId = other.mChannelId; mTitle = other.mTitle; mEpisodeTitle = other.mEpisodeTitle; mSeasonNumber = other.mSeasonNumber; + mSeasonTitle = other.mSeasonTitle; mEpisodeNumber = other.mEpisodeNumber; mStartTimeUtcMillis = other.mStartTimeUtcMillis; mEndTimeUtcMillis = other.mEndTimeUtcMillis; @@ -328,17 +346,19 @@ public final class Program implements Comparable<Program> { public static final class Builder { private final Program mProgram; + private long mId; public Builder() { mProgram = new Program(); // Fill initial data. mProgram.mChannelId = Channel.INVALID_ID; - mProgram.mTitle = "title"; - mProgram.mSeasonNumber = -1; - mProgram.mEpisodeNumber = -1; + mProgram.mTitle = null; + mProgram.mSeasonNumber = null; + mProgram.mSeasonTitle = null; + mProgram.mEpisodeNumber = null; mProgram.mStartTimeUtcMillis = -1; mProgram.mEndTimeUtcMillis = -1; - mProgram.mDescription = "description"; + mProgram.mDescription = null; } public Builder(Program other) { @@ -346,6 +366,11 @@ public final class Program implements Comparable<Program> { mProgram.copyFrom(other); } + public Builder setId(long id) { + mProgram.mId = id; + return this; + } + public Builder setChannelId(long channelId) { mProgram.mChannelId = channelId; return this; @@ -361,12 +386,17 @@ public final class Program implements Comparable<Program> { return this; } - public Builder setSeasonNumber(int seasonNumber) { + public Builder setSeasonNumber(String seasonNumber) { mProgram.mSeasonNumber = seasonNumber; return this; } - public Builder setEpisodeNumber(int episodeNumber) { + public Builder setSeasonTitle(String seasonTitle) { + mProgram.mSeasonTitle = seasonTitle; + return this; + } + + public Builder setEpisodeNumber(String episodeNumber) { mProgram.mEpisodeNumber = episodeNumber; return this; } @@ -473,11 +503,10 @@ public final class Program implements Comparable<Program> { boolean isDuplicate = p1.getChannelId() == p2.getChannelId() && p1.getStartTimeUtcMillis() == p2.getStartTimeUtcMillis() && p1.getEndTimeUtcMillis() == p2.getEndTimeUtcMillis(); - if (BuildConfig.ENG && isDuplicate) { + if (DEBUG && BuildConfig.ENG && isDuplicate) { Log.w(TAG, "Duplicate programs detected! - \"" + p1.getTitle() + "\" and \"" + p2.getTitle() + "\""); } return isDuplicate; } - } diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java index 6c167238..88db91b9 100644 --- a/src/com/android/tv/data/ProgramDataManager.java +++ b/src/com/android/tv/data/ProgramDataManager.java @@ -28,16 +28,17 @@ import android.os.Looper; import android.os.Message; import android.support.annotation.MainThread; import android.support.annotation.VisibleForTesting; +import android.util.ArraySet; import android.util.Log; import android.util.LongSparseArray; import android.util.LruCache; -import com.android.tv.common.CollectionUtils; import com.android.tv.common.MemoryManageable; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.data.epg.EpgFetcher; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.Clock; import com.android.tv.util.MultiLongSparseArray; -import com.android.tv.util.SoftPreconditions; import com.android.tv.util.Utils; import java.util.ArrayList; @@ -90,7 +91,7 @@ public class ProgramDataManager implements MemoryManageable { private final MultiLongSparseArray<OnCurrentProgramUpdatedListener> mChannelId2ProgramUpdatedListeners = new MultiLongSparseArray<>(); private final Handler mHandler; - private final Set<Listener> mListeners = CollectionUtils.createSmallSet(); + private final Set<Listener> mListeners = new ArraySet<>(); private final ContentObserver mProgramObserver; @@ -108,8 +109,12 @@ public class ProgramDataManager implements MemoryManageable { private boolean mPauseProgramUpdate = false; private final LruCache<Long, Program> mZeroLengthProgramCache = new LruCache<>(10); + // TODO: Change to final. + private EpgFetcher mEpgFetcher; + public ProgramDataManager(Context context) { this(context.getContentResolver(), Clock.SYSTEM, Looper.myLooper()); + mEpgFetcher = new EpgFetcher(context); } @VisibleForTesting @@ -128,8 +133,8 @@ public class ProgramDataManager implements MemoryManageable { } if (mPrefetchEnabled) { // The delay time of an existing MSG_UPDATE_PREFETCH_PROGRAM could be quite long - // up to PROGRAM_GUIDE_SNAP_TIME_MS. So we need to remove the existing message and - // send MSG_UPDATE_PREFETCH_PROGRAM again. + // up to PROGRAM_GUIDE_SNAP_TIME_MS. So we need to remove the existing message + // and send MSG_UPDATE_PREFETCH_PROGRAM again. mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM); mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); } @@ -169,6 +174,9 @@ public class ProgramDataManager implements MemoryManageable { } mContentResolver.registerContentObserver(Programs.CONTENT_URI, true, mProgramObserver); + if (mEpgFetcher != null) { + mEpgFetcher.start(); + } } /** @@ -182,6 +190,9 @@ public class ProgramDataManager implements MemoryManageable { } mStarted = false; + if (mEpgFetcher != null) { + mEpgFetcher.stop(); + } mContentResolver.unregisterContentObserver(mProgramObserver); mHandler.removeCallbacksAndMessages(null); @@ -201,6 +212,18 @@ public class ProgramDataManager implements MemoryManageable { } /** + * Reloads program data. + */ + public void reload() { + if (!mHandler.hasMessages(MSG_UPDATE_CURRENT_PROGRAMS)) { + mHandler.sendEmptyMessage(MSG_UPDATE_CURRENT_PROGRAMS); + } + if (mPrefetchEnabled && !mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) { + mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); + } + } + + /** * A listener interface to receive notification on program data retrieval from DB. */ public interface Listener { @@ -601,6 +624,22 @@ public class ProgramDataManager implements MemoryManageable { } } + /** + * Gets an single {@link Program} from {@link TvContract.Programs#CONTENT_URI}. + */ + public static class QueryProgramTask extends AsyncDbTask.AsyncQueryItemTask<Program> { + + public QueryProgramTask(ContentResolver contentResolver, long programId) { + super(contentResolver, TvContract.buildProgramUri(programId), Program.PROJECTION, null, + null, null); + } + + @Override + protected Program fromCursor(Cursor c) { + return Program.fromCursor(c); + } + } + private class MyHandler extends Handler { public MyHandler(Looper looper) { super(looper); diff --git a/src/com/android/tv/data/StreamInfo.java b/src/com/android/tv/data/StreamInfo.java index 04f8258a..df842737 100644 --- a/src/com/android/tv/data/StreamInfo.java +++ b/src/com/android/tv/data/StreamInfo.java @@ -30,6 +30,7 @@ public interface StreamInfo { int getVideoWidth(); int getVideoHeight(); float getVideoFrameRate(); + float getVideoDisplayAspectRatio(); int getVideoDefinitionLevel(); int getAudioChannelCount(); boolean hasClosedCaption(); diff --git a/src/com/android/tv/data/WatchedHistoryManager.java b/src/com/android/tv/data/WatchedHistoryManager.java index cff8cd5c..fc6672d2 100644 --- a/src/com/android/tv/data/WatchedHistoryManager.java +++ b/src/com/android/tv/data/WatchedHistoryManager.java @@ -3,10 +3,12 @@ package com.android.tv.data; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.support.annotation.MainThread; +import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.util.Log; @@ -42,8 +44,8 @@ public class WatchedHistoryManager { private boolean mStarted; private boolean mLoaded; private SharedPreferences mSharedPreferences; - private SharedPreferences.OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener = - new SharedPreferences.OnSharedPreferenceChangeListener() { + private final OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener = + new OnSharedPreferenceChangeListener() { @Override @MainThread public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, @@ -80,7 +82,7 @@ public class WatchedHistoryManager { private final Context mContext; private Listener mListener; private final int mMaxHistorySize; - private Handler mHandler; + private final Handler mHandler; public WatchedHistoryManager(Context context) { this(context, MAX_HISTORY_SIZE); @@ -197,6 +199,7 @@ public class WatchedHistoryManager { * Returns watched history in the ascending order of time. In other words, the first element * is the oldest and the last element is the latest record. */ + @NonNull public List<WatchedRecord> getWatchedHistory() { return Collections.unmodifiableList(mWatchedHistory); } diff --git a/src/com/android/tv/data/epg/EpgFetcher.java b/src/com/android/tv/data/epg/EpgFetcher.java new file mode 100644 index 00000000..9ff527d8 --- /dev/null +++ b/src/com/android/tv/data/epg/EpgFetcher.java @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.data.epg; + +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.media.tv.TvContract; +import android.media.tv.TvContract.Programs; +import android.media.tv.TvInputInfo; +import android.media.tv.TvInputManager.TvInputCallback; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.Log; + +import com.android.tv.Features; +import com.android.tv.TvApplication; +import com.android.tv.common.WeakHandler; +import com.android.tv.data.Channel; +import com.android.tv.data.Program; +import com.android.tv.util.RecurringRunner; +import com.android.tv.util.TvInputManagerHelper; +import com.android.tv.util.Utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * An utility class to fetch the EPG. This class isn't thread-safe. + */ +public class EpgFetcher { + private static final String TAG = "EpgFetcher"; + private static final boolean DEBUG = false; + + private static final int MSG_FETCH_EPG = 1; + + private static final long EPG_PREFETCH_RECURRING_PERIOD_MS = TimeUnit.HOURS.toMillis(4); + private static final long EPG_READER_INIT_WAIT_MS = TimeUnit.MINUTES.toMillis(1); + private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(30); + + private static final int BATCH_OPERATION_COUNT = 100; + + // Value: Long + private static final String KEY_LAST_UPDATED_EPG_TIMESTAMP = + "com.android.tv.data.epg.EpgFetcher.LastUpdatedEpgTimestamp"; + + private final Context mContext; + private final TvInputManagerHelper mInputHelper; + private final TvInputCallback mInputCallback; + private HandlerThread mHandlerThread; + private EpgFetcherHandler mHandler; + private RecurringRunner mRecurringRunner; + + private long mLastEpgTimestamp = -1; + + public EpgFetcher(Context context) { + mContext = context; + mInputHelper = TvApplication.getSingletons(mContext).getTvInputManagerHelper(); + mInputCallback = new TvInputCallback() { + @Override + public void onInputAdded(String inputId) { + if (Utils.isInternalTvInput(mContext, inputId)) { + mHandler.removeMessages(MSG_FETCH_EPG); + mHandler.sendEmptyMessage(MSG_FETCH_EPG); + } + } + }; + } + + /** + * Starts fetching EPG. + */ + public void start() { + if (DEBUG) Log.d(TAG, "Request to start fetching EPG."); + if (!Features.FETCH_EPG.isEnabled(mContext)) { + return; + } + if (mHandlerThread == null) { + mHandlerThread = new HandlerThread("EpgFetcher"); + mHandlerThread.start(); + mHandler = new EpgFetcherHandler(mHandlerThread.getLooper(), this); + mInputHelper.addCallback(mInputCallback); + mRecurringRunner = new RecurringRunner(mContext, EPG_PREFETCH_RECURRING_PERIOD_MS, + new Runnable() { + @Override + public void run() { + mHandler.removeMessages(MSG_FETCH_EPG); + mHandler.sendEmptyMessage(MSG_FETCH_EPG); + } + }, null); + mRecurringRunner.start(); + } + } + + /** + * Stops fetching EPG. + */ + public void stop() { + if (mHandlerThread == null) { + return; + } + mRecurringRunner.stop(); + mHandler.removeCallbacksAndMessages(null); + mHandler = null; + mHandlerThread.quit(); + mHandlerThread = null; + } + + private void onFetchEpg() { + if (DEBUG) Log.d(TAG, "Start fetching EPG."); + // Check for the internal inputs. + boolean hasInternalInput = false; + for (TvInputInfo input : mInputHelper.getTvInputInfos(true, true)) { + if (Utils.isInternalTvInput(mContext, input.getId())) { + hasInternalInput = true; + break; + } + } + if (!hasInternalInput) { + if (DEBUG) Log.d(TAG, "No internal input found."); + return; + } + // Check if EPG reader is available. + EpgReader epgReader = new StubEpgReader(mContext); + if (!epgReader.isAvailable()) { + if (DEBUG) Log.d(TAG, "EPG reader is not temporarily available."); + mHandler.removeMessages(MSG_FETCH_EPG); + mHandler.sendEmptyMessageDelayed(MSG_FETCH_EPG, EPG_READER_INIT_WAIT_MS); + return; + } + // Check the EPG Timestamp. + long epgTimestamp = epgReader.getEpgTimestamp(); + if (epgTimestamp <= getLastUpdatedEpgTimestamp()) { + if (DEBUG) Log.d(TAG, "No new EPG."); + return; + } + + List<Channel> channels = epgReader.getChannels(); + for (Channel channel : channels) { + List<Program> programs = new ArrayList<>(epgReader.getPrograms(channel.getId())); + Collections.sort(programs); + if (DEBUG) { + Log.d(TAG, "Fetching " + programs.size() + " programs for channel " + channel); + } + updateEpg(channel.getId(), programs); + } + + setLastUpdatedEpgTimestamp(epgTimestamp); + } + + private long getLastUpdatedEpgTimestamp() { + if (mLastEpgTimestamp < 0) { + mLastEpgTimestamp = PreferenceManager.getDefaultSharedPreferences(mContext).getLong( + KEY_LAST_UPDATED_EPG_TIMESTAMP, 0); + } + return mLastEpgTimestamp; + } + + private void setLastUpdatedEpgTimestamp(long timestamp) { + mLastEpgTimestamp = timestamp; + PreferenceManager.getDefaultSharedPreferences(mContext).edit().putLong( + KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp); + } + + private void updateEpg(long channelId, List<Program> newPrograms) { + final int fetchedProgramsCount = newPrograms.size(); + if (fetchedProgramsCount == 0) { + return; + } + long startTimeMs = System.currentTimeMillis(); + long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION; + List<Program> oldPrograms = queryPrograms(mContext.getContentResolver(), channelId, + startTimeMs, endTimeMs); + Program currentOldProgram = oldPrograms.size() > 0 ? oldPrograms.get(0) : null; + int oldProgramsIndex = 0; + int newProgramsIndex = 0; + // Skip the past programs. They will be automatically removed by the system. + if (currentOldProgram != null) { + long oldStartTimeUtcMillis = currentOldProgram.getStartTimeUtcMillis(); + for (Program program : newPrograms) { + if (program.getEndTimeUtcMillis() > oldStartTimeUtcMillis) { + break; + } + newProgramsIndex++; + } + } + // Compare the new programs with old programs one by one and update/delete the old one + // or insert new program if there is no matching program in the database. + ArrayList<ContentProviderOperation> ops = new ArrayList<>(); + while (newProgramsIndex < fetchedProgramsCount) { + // TODO: Extract to method and make test. + Program oldProgram = oldProgramsIndex < oldPrograms.size() + ? oldPrograms.get(oldProgramsIndex) : null; + Program newProgram = newPrograms.get(newProgramsIndex); + boolean addNewProgram = false; + if (oldProgram != null) { + if (oldProgram.equals(newProgram)) { + // Exact match. No need to update. Move on to the next programs. + oldProgramsIndex++; + newProgramsIndex++; + } else if (isSameTitleAndOverlap(oldProgram, newProgram)) { + if (!oldProgram.equals(oldProgram)) { + // Partial match. Update the old program with the new one. + // NOTE: Use 'update' in this case instead of 'insert' and 'delete'. There + // could be application specific settings which belong to the old program. + ops.add(ContentProviderOperation.newUpdate( + TvContract.buildProgramUri(oldProgram.getId())) + .withValues(toContentValues(newProgram)) + .build()); + } + oldProgramsIndex++; + newProgramsIndex++; + } else if (oldProgram.getEndTimeUtcMillis() + < newProgram.getEndTimeUtcMillis()) { + // No match. Remove the old program first to see if the next program in + // {@code oldPrograms} partially matches the new program. + ops.add(ContentProviderOperation.newDelete( + TvContract.buildProgramUri(oldProgram.getId())) + .build()); + oldProgramsIndex++; + } else { + // No match. The new program does not match any of the old programs. Insert + // it as a new program. + addNewProgram = true; + newProgramsIndex++; + } + } else { + // No old programs. Just insert new programs. + addNewProgram = true; + newProgramsIndex++; + } + if (addNewProgram) { + ops.add(ContentProviderOperation + .newInsert(TvContract.Programs.CONTENT_URI) + .withValues(toContentValues(newProgram)) + .build()); + } + // Throttle the batch operation not to cause TransactionTooLargeException. + if (ops.size() > BATCH_OPERATION_COUNT || newProgramsIndex >= fetchedProgramsCount) { + try { + if (DEBUG) { + int size = ops.size(); + Log.d(TAG, "Running " + size + " operations for channel " + channelId); + for (int i = 0; i < size; ++i) { + Log.d(TAG, "Operation(" + i + "): " + ops.get(i)); + } + } + mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops); + } catch (RemoteException | OperationApplicationException e) { + Log.e(TAG, "Failed to insert programs.", e); + return; + } + ops.clear(); + } + } + if (DEBUG) { + Log.d(TAG, "Fetched " + fetchedProgramsCount + " programs for channel " + channelId); + } + } + + private List<Program> queryPrograms(ContentResolver contentResolver, long channelId, + long startTimeMs, long endTimeMs) { + try (Cursor c = mContext.getContentResolver().query( + TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs), + Program.PROJECTION, null, null, Programs.COLUMN_START_TIME_UTC_MILLIS)) { + if (c == null) { + return Collections.EMPTY_LIST; + } + ArrayList<Program> programs = new ArrayList<>(); + while (c.moveToNext()) { + programs.add(Program.fromCursor(c)); + } + return programs; + } + } + + /** + * Returns {@code true} if the {@code oldProgram} program needs to be updated with the + * {@code newProgram} program. + */ + private boolean isSameTitleAndOverlap(Program oldProgram, Program newProgram) { + // NOTE: Here, we update the old program if it has the same title and overlaps with the + // new program. The test logic is just an example and you can modify this. E.g. check + // whether the both programs have the same program ID if your EPG supports any ID for + // the programs. + return Objects.equals(oldProgram.getTitle(), newProgram.getTitle()) + && oldProgram.getStartTimeUtcMillis() <= newProgram.getEndTimeUtcMillis() + && newProgram.getStartTimeUtcMillis() <= oldProgram.getEndTimeUtcMillis(); + } + + private static ContentValues toContentValues(Program program) { + ContentValues values = new ContentValues(); + values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId()); + putValue(values, TvContract.Programs.COLUMN_TITLE, program.getTitle()); + putValue(values, TvContract.Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle()); + putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber()); + putValue(values, TvContract.Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber()); + putValue(values, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription()); + putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri()); + values.put(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, + program.getStartTimeUtcMillis()); + values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis()); + return values; + } + + private static void putValue(ContentValues contentValues, String key, String value) { + if (TextUtils.isEmpty(value)) { + contentValues.putNull(key); + } else { + contentValues.put(key, value); + } + } + + private static class EpgFetcherHandler extends WeakHandler<EpgFetcher> { + public EpgFetcherHandler (@NonNull Looper looper, EpgFetcher ref) { + super(looper, ref); + } + + @Override + public void handleMessage(Message msg, @NonNull EpgFetcher epgFetcher) { + switch (msg.what) { + case MSG_FETCH_EPG: + epgFetcher.onFetchEpg(); + break; + default: + super.handleMessage(msg); + break; + } + } + } +} diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java new file mode 100644 index 00000000..1c7712f4 --- /dev/null +++ b/src/com/android/tv/data/epg/EpgReader.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.data.epg; + +import android.support.annotation.WorkerThread; + +import com.android.tv.data.Channel; +import com.android.tv.data.Program; + +import java.util.List; + +/** + * An interface used to retrieve the EPG data. This class should be used in worker thread. + */ +@WorkerThread +public interface EpgReader { + /** + * Checks if the reader is available. + */ + boolean isAvailable(); + + /** + * Returns the timestamp of the current EPG. + * The format should be YYYYMMDDHHmmSS as a long value. ex) 20160308141500 + */ + long getEpgTimestamp(); + + /** + * Returns the channels list. + */ + List<Channel> getChannels(); + + /** + * Returns the programs for the given channel. The result is sorted by the start time. + * Note that the {@code Program} doesn't have valid program ID because it's not retrieved from + * TvProvider. + */ + List<Program> getPrograms(long channelId); +} diff --git a/src/com/android/tv/data/epg/StubEpgReader.java b/src/com/android/tv/data/epg/StubEpgReader.java new file mode 100644 index 00000000..2896e8e5 --- /dev/null +++ b/src/com/android/tv/data/epg/StubEpgReader.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.data.epg; + +import android.content.Context; + +import com.android.tv.data.Channel; +import com.android.tv.data.Program; + +import java.util.Collections; +import java.util.List; + +/** + * A stub class to read EPG. + */ +public class StubEpgReader implements EpgReader{ + public StubEpgReader(Context context) { + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public long getEpgTimestamp() { + return 0; + } + + @Override + public List<Channel> getChannels() { + return Collections.EMPTY_LIST; + } + + @Override + public List<Program> getPrograms(long channelId) { + return Collections.EMPTY_LIST; + } +} diff --git a/src/com/android/tv/dialog/FullscreenDialogFragment.java b/src/com/android/tv/dialog/FullscreenDialogFragment.java index eb84aaf9..d16202a1 100644 --- a/src/com/android/tv/dialog/FullscreenDialogFragment.java +++ b/src/com/android/tv/dialog/FullscreenDialogFragment.java @@ -48,7 +48,6 @@ public class FullscreenDialogFragment extends SafeDismissDialogFragment { return f; } - private int mViewLayoutResId; private String mTrackerLabel; private DialogView mDialogView; @@ -58,9 +57,9 @@ public class FullscreenDialogFragment extends SafeDismissDialogFragment { new FullscreenDialog(getActivity(), R.style.Theme_TV_dialog_Fullscreen); LayoutInflater inflater = LayoutInflater.from(getActivity()); Bundle args = getArguments(); - mViewLayoutResId = args.getInt(VIEW_LAYOUT_ID); mTrackerLabel = args.getString(TRACKER_LABEL); - View v = inflater.inflate(mViewLayoutResId, null); + int viewLayoutResId = args.getInt(VIEW_LAYOUT_ID); + View v = inflater.inflate(viewLayoutResId, null); dialog.setContentView(v); mDialogView = (DialogView) v; mDialogView.initialize((MainActivity) getActivity(), dialog); diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java index a98b5fa0..0fb469be 100644 --- a/src/com/android/tv/dvr/BaseDvrDataManager.java +++ b/src/com/android/tv/dvr/BaseDvrDataManager.java @@ -16,68 +16,155 @@ package com.android.tv.dvr; +import android.annotation.TargetApi; import android.content.Context; +import android.os.Build; import android.support.annotation.MainThread; +import android.util.ArraySet; import android.util.Log; -import com.android.tv.common.CollectionUtils; +import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.util.SoftPreconditions; +import com.android.tv.common.recording.RecordedProgram; +import com.android.tv.util.Clock; +import java.util.ArrayList; +import java.util.List; import java.util.Set; /** * Base implementation of @{link DataManagerInternal}. */ @MainThread +@TargetApi(Build.VERSION_CODES.N) public abstract class BaseDvrDataManager implements WritableDvrDataManager { private final static String TAG = "BaseDvrDataManager"; private final static boolean DEBUG = false; + protected final Clock mClock; - private final Set<DvrDataManager.Listener> mListeners = CollectionUtils.createSmallSet(); + private final Set<ScheduledRecordingListener> mScheduledRecordingListeners = new ArraySet<>(); + private final Set<RecordedProgramListener> mRecordedProgramListeners = new ArraySet<>(); - BaseDvrDataManager (Context context){ + BaseDvrDataManager(Context context, Clock clock) { SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG); + mClock = clock; } @Override - public final void addListener(DvrDataManager.Listener listener) { - mListeners.add(listener); + public final void addScheduledRecordingListener(ScheduledRecordingListener listener) { + mScheduledRecordingListeners.add(listener); } @Override - public final void removeListener(DvrDataManager.Listener listener) { - mListeners.remove(listener); + public final void removeScheduledRecordingListener(ScheduledRecordingListener listener) { + mScheduledRecordingListeners.remove(listener); + } + + @Override + public final void addRecordedProgramListener(RecordedProgramListener listener) { + mRecordedProgramListeners.add(listener); + } + + @Override + public final void removeRecordedProgramListener(RecordedProgramListener listener) { + mRecordedProgramListeners.remove(listener); } /** - * Calls {@link DvrDataManager.Listener#onRecordingAdded(Recording)} for each current listener. + * Calls {@link RecordedProgramListener#onRecordedProgramAdded(RecordedProgram)} + * for each listener. */ - protected final void notifyRecordingAdded(Recording recording) { - for (Listener l : mListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "added recording " + recording); - l.onRecordingAdded(recording); + protected final void notifyRecordedProgramAdded(RecordedProgram recordedProgram) { + for (RecordedProgramListener l : mRecordedProgramListeners) { + if (DEBUG) Log.d(TAG, "notify " + l + "added " + recordedProgram); + l.onRecordedProgramAdded(recordedProgram); } } /** - * Calls {@link DvrDataManager.Listener#onRecordingRemoved(Recording)} for each current listener. + * Calls {@link RecordedProgramListener#onRecordedProgramChanged(RecordedProgram)} + * for each listener. */ - protected final void notifyRecordingRemoved(Recording recording) { - for (Listener l : mListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "removed recording " + recording); - l.onRecordingRemoved(recording); + protected final void notifyRecordedProgramChanged(RecordedProgram recordedProgram) { + for (RecordedProgramListener l : mRecordedProgramListeners) { + if (DEBUG) Log.d(TAG, "notify " + l + "changed " + recordedProgram); + l.onRecordedProgramChanged(recordedProgram); } } /** - * Calls {@link DvrDataManager.Listener#onRecordingStatusChanged(Recording)} for each current - * listener. + * Calls {@link RecordedProgramListener#onRecordedProgramRemoved(RecordedProgram)} + * for each listener. */ - protected final void notifyRecordingStatusChanged(Recording recording) { - for (Listener l : mListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "changed recording " + recording); - l.onRecordingStatusChanged(recording); + protected final void notifyRecordedProgramRemoved(RecordedProgram recordedProgram) { + for (RecordedProgramListener l : mRecordedProgramListeners) { + if (DEBUG) Log.d(TAG, "notify " + l + "removed " + recordedProgram); + l.onRecordedProgramRemoved(recordedProgram); } } + + /** + * Calls {@link ScheduledRecordingListener#onScheduledRecordingAdded(ScheduledRecording)} + * for each listener. + */ + protected final void notifyScheduledRecordingAdded(ScheduledRecording scheduledRecording) { + for (ScheduledRecordingListener l : mScheduledRecordingListeners) { + if (DEBUG) Log.d(TAG, "notify " + l + "added " + scheduledRecording); + l.onScheduledRecordingAdded(scheduledRecording); + } + } + + /** + * Calls {@link ScheduledRecordingListener#onScheduledRecordingRemoved(ScheduledRecording)} + * for each listener. + */ + protected final void notifyScheduledRecordingRemoved(ScheduledRecording scheduledRecording) { + for (ScheduledRecordingListener l : mScheduledRecordingListeners) { + if (DEBUG) { + Log.d(TAG, "notify " + l + "removed " + scheduledRecording); + } + l.onScheduledRecordingRemoved(scheduledRecording); + } + } + + /** + * Calls + * {@link ScheduledRecordingListener#onScheduledRecordingStatusChanged(ScheduledRecording)} + * for each listener. + */ + protected final void notifyScheduledRecordingStatusChanged( + ScheduledRecording scheduledRecording) { + for (ScheduledRecordingListener l : mScheduledRecordingListeners) { + if (DEBUG) Log.d(TAG, "notify " + l + "changed " + scheduledRecording); + l.onScheduledRecordingStatusChanged(scheduledRecording); + } + } + + /** + * Returns a new list with only {@link ScheduledRecording} with a {@link + * ScheduledRecording#getEndTimeMs() endTime} after now. + */ + private List<ScheduledRecording> filterEndTimeIsPast(List<ScheduledRecording> originals) { + List<ScheduledRecording> results = new ArrayList<>(originals.size()); + for (ScheduledRecording r : originals) { + if (r.getEndTimeMs() > mClock.currentTimeMillis()) { + results.add(r); + } + } + return results; + } + + @Override + public List<ScheduledRecording> getStartedRecordings() { + return filterEndTimeIsPast( + getRecordingsWithState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS)); + } + + @Override + public List<ScheduledRecording> getNonStartedScheduledRecordings() { + return filterEndTimeIsPast( + getRecordingsWithState(ScheduledRecording.STATE_RECORDING_NOT_STARTED)); + } + + protected abstract List<ScheduledRecording> getRecordingsWithState(int state); } diff --git a/src/com/android/tv/dvr/DvrDataManager.java b/src/com/android/tv/dvr/DvrDataManager.java index 4f8b0525..c96104e5 100644 --- a/src/com/android/tv/dvr/DvrDataManager.java +++ b/src/com/android/tv/dvr/DvrDataManager.java @@ -20,6 +20,8 @@ import android.support.annotation.MainThread; import android.support.annotation.Nullable; import android.util.Range; +import com.android.tv.common.recording.RecordedProgram; + import java.util.List; /** @@ -32,24 +34,24 @@ public interface DvrDataManager { boolean isInitialized(); /** - * Returns recordings. + * Returns past recordings. */ - List<Recording> getRecordings(); + List<RecordedProgram> getRecordedPrograms(); /** - * Returns past recordings. + * Returns all {@link ScheduledRecording} regardless of state. */ - List<Recording> getFinishedRecordings(); + List<ScheduledRecording> getAllScheduledRecordings(); /** - * Returns started recordings. + * Returns started recordings that expired. */ - List<Recording> getStartedRecordings(); + List<ScheduledRecording> getStartedRecordings(); /** - * Returns scheduled recordings + * Returns scheduled but not started recordings that have not expired. */ - List<Recording> getScheduledRecordings(); + List<ScheduledRecording> getNonStartedScheduledRecordings(); /** * Returns season recordings. @@ -73,27 +75,60 @@ public interface DvrDataManager { * * @param period a time period in milliseconds. */ - List<Recording> getRecordingsThatOverlapWith(Range<Long> period); + List<ScheduledRecording> getRecordingsThatOverlapWith(Range<Long> period); + + /** + * Add a {@link ScheduledRecordingListener}. + */ + void addScheduledRecordingListener(ScheduledRecordingListener scheduledRecordingListener); + + /** + * Remove a {@link ScheduledRecordingListener}. + */ + void removeScheduledRecordingListener(ScheduledRecordingListener scheduledRecordingListener); + + /** + * Add a {@link RecordedProgramListener}. + */ + void addRecordedProgramListener(RecordedProgramListener listener); /** - * Add a {@link Listener}. + * Remove a {@link RecordedProgramListener}. */ - void addListener(Listener listener); + void removeRecordedProgramListener(RecordedProgramListener listener); /** - * Remove a {@link Listener}. + * Returns the scheduled recording program with the given recordingId or null if is not found. */ - void removeListener(Listener listener); + @Nullable + ScheduledRecording getScheduledRecording(long recordingId); + + + /** + * Returns the scheduled recording program with the given programId or null if is not found. + */ + @Nullable + ScheduledRecording getScheduledRecordingForProgramId(long programId); /** - * Returns the recording with the given recordingId or null if is not found + * Returns the recorded program with the given recordingId or null if is not found. */ @Nullable - Recording getRecording(long recordingId); + RecordedProgram getRecordedProgram(long recordingId); + + interface ScheduledRecordingListener { + void onScheduledRecordingAdded(ScheduledRecording scheduledRecording); + + void onScheduledRecordingRemoved(ScheduledRecording scheduledRecording); + + void onScheduledRecordingStatusChanged(ScheduledRecording scheduledRecording); + } + + interface RecordedProgramListener { + void onRecordedProgramAdded(RecordedProgram recordedProgram); + + void onRecordedProgramChanged(RecordedProgram recordedProgram); - interface Listener { - void onRecordingAdded(Recording recording); - void onRecordingRemoved(Recording recording); - void onRecordingStatusChanged(Recording recording); + void onRecordedProgramRemoved(RecordedProgram recordedProgram); } } diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java index 647d9bd7..02c47750 100644 --- a/src/com/android/tv/dvr/DvrDataManagerImpl.java +++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java @@ -16,95 +16,174 @@ package com.android.tv.dvr; +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.ContentUris; import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.media.tv.TvContract; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; import android.support.annotation.MainThread; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; +import android.util.ArraySet; import android.util.Log; import android.util.Range; -import com.android.tv.dvr.Recording.RecordingState; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.recording.RecordedProgram; +import com.android.tv.dvr.ScheduledRecording.RecordingState; import com.android.tv.dvr.provider.AsyncDvrDbTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryTask; -import com.android.tv.util.SoftPreconditions; +import com.android.tv.util.AsyncDbTask; +import com.android.tv.util.Clock; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.Set; /** * DVR Data manager to handle recordings and schedules. */ @MainThread +@TargetApi(Build.VERSION_CODES.N) public class DvrDataManagerImpl extends BaseDvrDataManager { private static final String TAG = "DvrDataManagerImpl"; + private static final boolean DEBUG = false; - private Context mContext; - private boolean mLoadFinished; - private final HashMap<Long, Recording> mRecordings = new HashMap<>(); - private AsyncDvrQueryTask mQueryTask; + private final HashMap<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>(); + private final HashMap<Long, ScheduledRecording> mProgramId2ScheduledRecordings = + new HashMap<>(); + private final HashMap<Long, RecordedProgram> mRecordedPrograms = new HashMap<>(); - public DvrDataManagerImpl(Context context) { - super(context); + private final Context mContext; + private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); + private final ContentObserver mContentObserver = new ContentObserver(mMainThreadHandler) { + + @Override + public void onChange(boolean selfChange) { + onChange(selfChange, null); + } + + @Override + public void onChange(boolean selfChange, @Nullable final Uri uri) { + if (uri == null) { + // TODO reload everything. + } + AsyncRecordedProgramQueryTask task = new AsyncRecordedProgramQueryTask( + mContext.getContentResolver(), uri); + task.executeOnDbThread(); + mPendingTasks.add(task); + } + }; + + private void onObservedChange(Uri uri, RecordedProgram recordedProgram) { + long id = ContentUris.parseId(uri); + if (DEBUG) { + Log.d(TAG, "changed recorded program #" + id + " to " + recordedProgram); + } + if (recordedProgram == null) { + RecordedProgram old = mRecordedPrograms.remove(id); + if (old != null) { + notifyRecordedProgramRemoved(old); + } else { + Log.w(TAG, "Could not find old version of deleted program #" + id); + } + } else { + RecordedProgram old = mRecordedPrograms.put(id, recordedProgram); + if (old == null) { + notifyRecordedProgramAdded(recordedProgram); + } else { + notifyRecordedProgramChanged(recordedProgram); + } + } + } + + private boolean mDvrLoadFinished; + private boolean mRecordedProgramLoadFinished; + private final Set<AsyncTask> mPendingTasks = new ArraySet<>(); + + public DvrDataManagerImpl(Context context, Clock clock) { + super(context, clock); mContext = context; } public void start() { - mQueryTask = new AsyncDvrQueryTask(mContext) { + AsyncDvrQueryTask mDvrQueryTask = new AsyncDvrQueryTask(mContext) { + @Override - protected void onPostExecute(List<Recording> result) { - mQueryTask = null; - mLoadFinished = true; - for (Recording r : result) { - mRecordings.put(r.getId(), r); + protected void onCancelled(List<ScheduledRecording> scheduledRecordings) { + mPendingTasks.remove(this); + } + + @Override + protected void onPostExecute(List<ScheduledRecording> result) { + mPendingTasks.remove(this); + mDvrLoadFinished = true; + for (ScheduledRecording r : result) { + mScheduledRecordings.put(r.getId(), r); } } }; - mQueryTask.executeOnDbThread(); + mDvrQueryTask.executeOnDbThread(); + mPendingTasks.add(mDvrQueryTask); + AsyncRecordedProgramsQueryTask mRecordedProgramQueryTask = + new AsyncRecordedProgramsQueryTask(mContext.getContentResolver()); + mRecordedProgramQueryTask.executeOnDbThread(); + ContentResolver cr = mContext.getContentResolver(); + cr.registerContentObserver(TvContract.RecordedPrograms.CONTENT_URI, true, mContentObserver); } public void stop() { - if (mQueryTask != null) { - mQueryTask.cancel(true); - mQueryTask = null; + ContentResolver cr = mContext.getContentResolver(); + cr.unregisterContentObserver(mContentObserver); + Iterator<AsyncTask> i = mPendingTasks.iterator(); + while (i.hasNext()) { + AsyncTask task = i.next(); + i.remove(); + task.cancel(true); } } @Override public boolean isInitialized() { - return mLoadFinished; + return mDvrLoadFinished && mRecordedProgramLoadFinished; } - @Override - public List<Recording> getRecordings() { - if (!mLoadFinished) { + private List<ScheduledRecording> getScheduledRecordingsPrograms() { + if (!mDvrLoadFinished) { return Collections.emptyList(); } - ArrayList<Recording> list = new ArrayList<>(mRecordings.size()); - list.addAll(mRecordings.values()); - Collections.sort(list, Recording.START_TIME_COMPARATOR); - return Collections.unmodifiableList(list); - } - - @Override - public List<Recording> getFinishedRecordings() { - return getRecordingsWithState(Recording.STATE_RECORDING_FINISHED); + ArrayList<ScheduledRecording> list = new ArrayList<>(mScheduledRecordings.size()); + list.addAll(mScheduledRecordings.values()); + Collections.sort(list, ScheduledRecording.START_TIME_COMPARATOR); + return list; } @Override - public List<Recording> getStartedRecordings() { - return getRecordingsWithState(Recording.STATE_RECORDING_IN_PROGRESS); + public List<RecordedProgram> getRecordedPrograms() { + if (!mRecordedProgramLoadFinished) { + return Collections.emptyList(); + } + return new ArrayList<>(mRecordedPrograms.values()); } @Override - public List<Recording> getScheduledRecordings() { - return getRecordingsWithState(Recording.STATE_RECORDING_NOT_STARTED); + public List<ScheduledRecording> getAllScheduledRecordings() { + return new ArrayList<>(mScheduledRecordings.values()); } - private List<Recording> getRecordingsWithState(@RecordingState int state) { - List<Recording> result = new ArrayList<>(); - for (Recording r : mRecordings.values()) { + protected List<ScheduledRecording> getRecordingsWithState(@RecordingState int state) { + List<ScheduledRecording> result = new ArrayList<>(); + for (ScheduledRecording r : mScheduledRecordings.values()) { if (r.getState() == state) { result.add(r); } @@ -120,29 +199,29 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { @Override public long getNextScheduledStartTimeAfter(long startTime) { - return getNextStartTimeAfter(getRecordings(), startTime); + return getNextStartTimeAfter(getScheduledRecordingsPrograms(), startTime); } @VisibleForTesting - static long getNextStartTimeAfter(List<Recording> recordings, long startTime) { + static long getNextStartTimeAfter(List<ScheduledRecording> scheduledRecordings, long startTime) { int start = 0; - int end = recordings.size() - 1; + int end = scheduledRecordings.size() - 1; while (start <= end) { int mid = (start + end) / 2; - if (recordings.get(mid).getStartTimeMs() <= startTime) { + if (scheduledRecordings.get(mid).getStartTimeMs() <= startTime) { start = mid + 1; } else { end = mid - 1; } } - return start < recordings.size() ? recordings.get(start).getStartTimeMs() + return start < scheduledRecordings.size() ? scheduledRecordings.get(start).getStartTimeMs() : NEXT_START_TIME_NOT_FOUND; } @Override - public List<Recording> getRecordingsThatOverlapWith(Range<Long> period) { - List<Recording> result = new ArrayList<>(); - for (Recording r : mRecordings.values()) { + public List<ScheduledRecording> getRecordingsThatOverlapWith(Range<Long> period) { + List<ScheduledRecording> result = new ArrayList<>(); + for (ScheduledRecording r : mScheduledRecordings.values()) { if (r.isOverLapping(period)) { result.add(r); } @@ -152,38 +231,56 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { @Nullable @Override - public Recording getRecording(long recordingId) { - if (mLoadFinished) { - return mRecordings.get(recordingId); + public ScheduledRecording getScheduledRecording(long recordingId) { + if (mDvrLoadFinished) { + return mScheduledRecordings.get(recordingId); + } + return null; + } + + @Nullable + @Override + public ScheduledRecording getScheduledRecordingForProgramId(long programId) { + if (mDvrLoadFinished) { + return mProgramId2ScheduledRecordings.get(programId); } return null; } + @Nullable + @Override + public RecordedProgram getRecordedProgram(long recordingId) { + return mRecordedPrograms.get(recordingId); + } + @Override - public void addRecording(final Recording recording) { + public void addScheduledRecording(final ScheduledRecording scheduledRecording) { new AsyncDvrDbTask.AsyncAddRecordingTask(mContext) { @Override - protected void onPostExecute(List<Recording> recordings) { - super.onPostExecute(recordings); - SoftPreconditions.checkArgument(recordings.size() == 1); - for (Recording r : recordings) { + protected void onPostExecute(List<ScheduledRecording> scheduledRecordings) { + super.onPostExecute(scheduledRecordings); + SoftPreconditions.checkArgument(scheduledRecordings.size() == 1); + for (ScheduledRecording r : scheduledRecordings) { if (r.getId() != -1) { - mRecordings.put(r.getId(), r); - notifyRecordingAdded(r); + mScheduledRecordings.put(r.getId(), r); + if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) { + mProgramId2ScheduledRecordings.put(r.getProgramId(), r); + } + notifyScheduledRecordingAdded(r); } else { Log.w(TAG, "Error adding " + r); } } } - }.executeOnDbThread(recording); + }.executeOnDbThread(scheduledRecording); } @Override public void addSeasonRecording(SeasonRecording seasonRecording) { } @Override - public void removeRecording(final Recording recording) { + public void removeScheduledRecording(final ScheduledRecording scheduledRecording) { new AsyncDvrDbTask.AsyncDeleteRecordingTask(mContext) { @Override protected void onPostExecute(List<Integer> counts) { @@ -191,23 +288,27 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { SoftPreconditions.checkArgument(counts.size() == 1); for (Integer c : counts) { if (c == 1) { - mRecordings.remove(recording.getId()); + mScheduledRecordings.remove(scheduledRecording.getId()); + if (scheduledRecording.getProgramId() != ScheduledRecording.ID_NOT_SET) { + mProgramId2ScheduledRecordings + .remove(scheduledRecording.getProgramId()); + } //TODO change to notifyRecordingUpdated - notifyRecordingRemoved(recording); + notifyScheduledRecordingRemoved(scheduledRecording); } else { - Log.w(TAG, "Error removing " + recording); + Log.w(TAG, "Error removing " + scheduledRecording); } } } - }.executeOnDbThread(recording); + }.executeOnDbThread(scheduledRecording); } @Override public void removeSeasonSchedule(SeasonRecording seasonSchedule) { } @Override - public void updateRecording(final Recording recording) { + public void updateScheduledRecording(final ScheduledRecording scheduledRecording) { new AsyncDvrDbTask.AsyncUpdateRecordingTask(mContext) { @Override protected void onPostExecute(List<Integer> counts) { @@ -215,15 +316,88 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { SoftPreconditions.checkArgument(counts.size() == 1); for (Integer c : counts) { if (c == 1) { - mRecordings.put(recording.getId(), recording); + ScheduledRecording oldScheduledRecording = mScheduledRecordings + .put(scheduledRecording.getId(), scheduledRecording); + long programId = scheduledRecording.getProgramId(); + if (oldScheduledRecording != null + && oldScheduledRecording.getProgramId() != programId + && oldScheduledRecording.getProgramId() + != ScheduledRecording.ID_NOT_SET) { + ScheduledRecording oldValueForProgramId = mProgramId2ScheduledRecordings + .get(oldScheduledRecording.getProgramId()); + if (oldValueForProgramId.getId() == scheduledRecording.getId()) { + //Only remove the old ScheduledRecording if it has the same ID as + // the new one. + mProgramId2ScheduledRecordings + .remove(oldScheduledRecording.getProgramId()); + } + } + if (programId != ScheduledRecording.ID_NOT_SET) { + mProgramId2ScheduledRecordings.put(programId, scheduledRecording); + } //TODO change to notifyRecordingUpdated - notifyRecordingStatusChanged(recording); + notifyScheduledRecordingStatusChanged(scheduledRecording); } else { - Log.w(TAG, "Error updating " + recording); + Log.w(TAG, "Error updating " + scheduledRecording); } } + } + }.executeOnDbThread(scheduledRecording); + } + + private final class AsyncRecordedProgramsQueryTask + extends AsyncDbTask.AsyncQueryListTask<RecordedProgram> { + public AsyncRecordedProgramsQueryTask(ContentResolver contentResolver) { + super(contentResolver, TvContract.RecordedPrograms.CONTENT_URI, + RecordedProgram.PROJECTION, null, null, null); + } + + @Override + protected RecordedProgram fromCursor(Cursor c) { + return RecordedProgram.fromCursor(c); + } + + @Override + protected void onCancelled(List<RecordedProgram> scheduledRecordings) { + mPendingTasks.remove(this); + } + @Override + protected void onPostExecute(List<RecordedProgram> result) { + mPendingTasks.remove(this); + mRecordedProgramLoadFinished = true; + if (result != null) { + for (RecordedProgram r : result) { + mRecordedPrograms.put(r.getId(), r); + } } - }.executeOnDbThread(recording); + } + } + + private final class AsyncRecordedProgramQueryTask + extends AsyncDbTask.AsyncQueryItemTask<RecordedProgram> { + + private final Uri mUri; + + public AsyncRecordedProgramQueryTask(ContentResolver contentResolver, Uri uri) { + super(contentResolver, uri, RecordedProgram.PROJECTION, null, null, null); + mUri = uri; + } + + @Override + protected RecordedProgram fromCursor(Cursor c) { + return RecordedProgram.fromCursor(c); + } + + @Override + protected void onCancelled(RecordedProgram recordedProgram) { + mPendingTasks.remove(this); + } + + @Override + protected void onPostExecute(RecordedProgram recordedProgram) { + mPendingTasks.remove(this); + onObservedChange(mUri, recordedProgram); + } } } diff --git a/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java b/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java index 8a19cb29..95b342bb 100644 --- a/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java +++ b/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java @@ -23,7 +23,9 @@ import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.Range; -import com.android.tv.util.SoftPreconditions; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.recording.RecordedProgram; +import com.android.tv.util.Clock; import java.util.ArrayList; import java.util.Collections; @@ -40,11 +42,12 @@ import java.util.concurrent.atomic.AtomicLong; public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager { private final static String TAG = "DvrDataManagerInMemory"; private final AtomicLong mNextId = new AtomicLong(1); - private final Map<Long, Recording> mRecordings = new HashMap<>(); - private List<SeasonRecording> mSeasonSchedule = new ArrayList<>(); + private final Map<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>(); + private final Map<Long, RecordedProgram> mRecordedPrograms = new HashMap<>(); + private final List<SeasonRecording> mSeasonSchedule = new ArrayList<>(); - public DvrDataManagerInMemoryImpl(Context context) { - super(context); + public DvrDataManagerInMemoryImpl(Context context, Clock clock) { + super(context, clock); } @Override @@ -52,27 +55,20 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager { return true; } - @Override - public List<Recording> getRecordings() { - return new ArrayList(mRecordings.values()); - } - - @Override - public List<Recording> getFinishedRecordings() { - return getRecordingsWithState(Recording.STATE_RECORDING_FINISHED); + private List<ScheduledRecording> getScheduledRecordingsPrograms() { + return new ArrayList(mScheduledRecordings.values()); } @Override - public List<Recording> getStartedRecordings() { - return getRecordingsWithState(Recording.STATE_RECORDING_IN_PROGRESS); + public List<RecordedProgram> getRecordedPrograms() { + return new ArrayList<>(mRecordedPrograms.values()); } @Override - public List<Recording> getScheduledRecordings() { - return getRecordingsWithState(Recording.STATE_RECORDING_NOT_STARTED); + public List<ScheduledRecording> getAllScheduledRecordings() { + return new ArrayList<>(mScheduledRecordings.values()); } - @Override public List<SeasonRecording> getSeasonRecordings() { return mSeasonSchedule; } @@ -80,9 +76,9 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager { @Override public long getNextScheduledStartTimeAfter(long startTime) { - List<Recording> temp = getScheduledRecordings(); - Collections.sort(temp, Recording.START_TIME_COMPARATOR); - for (Recording r : temp) { + List<ScheduledRecording> temp = getNonStartedScheduledRecordings(); + Collections.sort(temp, ScheduledRecording.START_TIME_COMPARATOR); + for (ScheduledRecording r : temp) { if (r.getStartTimeMs() > startTime) { return r.getStartTimeMs(); } @@ -91,10 +87,10 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager { } @Override - public List<Recording> getRecordingsThatOverlapWith(Range<Long> period) { - List<Recording> temp = getRecordings(); - List<Recording> result = new ArrayList<>(); - for (Recording r : temp) { + public List<ScheduledRecording> getRecordingsThatOverlapWith(Range<Long> period) { + List<ScheduledRecording> temp = getScheduledRecordingsPrograms(); + List<ScheduledRecording> result = new ArrayList<>(); + for (ScheduledRecording r : temp) { if (r.isOverLapping(period)) { result.add(r); } @@ -103,20 +99,56 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager { } /** - * Add a new recording. + * Add a new scheduled recording. */ @Override - public void addRecording(Recording recording) { - addRecordingInternal(recording); + public void addScheduledRecording(ScheduledRecording scheduledRecording) { + addScheduledRecordingInternal(scheduledRecording); + } + + + public void addRecordedProgram(RecordedProgram recordedProgram) { + addRecordedProgramInternal(recordedProgram); + } + + public void updateRecordedProgram(RecordedProgram r) { + long id = r.getId(); + if (mRecordedPrograms.containsKey(id)) { + mRecordedPrograms.put(id, r); + notifyRecordedProgramChanged(r); + } else { + throw new IllegalArgumentException("Recording not found:" + r); + } + } + + public void removeRecordedProgram(RecordedProgram scheduledRecording) { + mRecordedPrograms.remove(scheduledRecording.getId()); + notifyRecordedProgramRemoved(scheduledRecording); + } + + + public ScheduledRecording addScheduledRecordingInternal(ScheduledRecording scheduledRecording) { + SoftPreconditions + .checkState(scheduledRecording.getId() == ScheduledRecording.ID_NOT_SET, TAG, + "expected id of " + ScheduledRecording.ID_NOT_SET + " but was " + + scheduledRecording); + scheduledRecording = ScheduledRecording.buildFrom(scheduledRecording) + .setId(mNextId.incrementAndGet()) + .build(); + mScheduledRecordings.put(scheduledRecording.getId(), scheduledRecording); + notifyScheduledRecordingAdded(scheduledRecording); + return scheduledRecording; } - public Recording addRecordingInternal(Recording recording) { - SoftPreconditions.checkState(recording.getId() == Recording.ID_NOT_SET, TAG, - "expected id of " + Recording.ID_NOT_SET + " but was " + recording); - recording = Recording.buildFrom(recording).setId(mNextId.incrementAndGet()).build(); - mRecordings.put(recording.getId(), recording); - notifyRecordingAdded(recording); - return recording; + public RecordedProgram addRecordedProgramInternal(RecordedProgram recordedProgram) { + SoftPreconditions.checkState(recordedProgram.getId() == RecordedProgram.ID_NOT_SET, TAG, + "expected id of " + RecordedProgram.ID_NOT_SET + " but was " + recordedProgram); + recordedProgram = RecordedProgram.buildFrom(recordedProgram) + .setId(mNextId.incrementAndGet()) + .build(); + mRecordedPrograms.put(recordedProgram.getId(), recordedProgram); + notifyRecordedProgramAdded(recordedProgram); + return recordedProgram; } @Override @@ -125,9 +157,9 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager { } @Override - public void removeRecording(Recording recording) { - mRecordings.remove(recording.getId()); - notifyRecordingRemoved(recording); + public void removeScheduledRecording(ScheduledRecording scheduledRecording) { + mScheduledRecordings.remove(scheduledRecording.getId()); + notifyScheduledRecordingRemoved(scheduledRecording); } @Override @@ -136,11 +168,11 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager { } @Override - public void updateRecording(Recording r) { + public void updateScheduledRecording(ScheduledRecording r) { long id = r.getId(); - if (mRecordings.containsKey(id)) { - mRecordings.put(id, r); - notifyRecordingStatusChanged(r); + if (mScheduledRecordings.containsKey(id)) { + mScheduledRecordings.put(id, r); + notifyScheduledRecordingStatusChanged(r); } else { throw new IllegalArgumentException("Recording not found:" + r); } @@ -148,14 +180,32 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager { @Nullable @Override - public Recording getRecording(long id) { - return mRecordings.get(id); + public ScheduledRecording getScheduledRecording(long id) { + return mScheduledRecordings.get(id); } + @Nullable + @Override + public ScheduledRecording getScheduledRecordingForProgramId(long programId) { + for (ScheduledRecording r : mScheduledRecordings.values()) { + if (r.getProgramId() == programId) { + return r; + } + } + return null; + } + + @Nullable + @Override + public RecordedProgram getRecordedProgram(long recordingId) { + return mRecordedPrograms.get(recordingId); + } + + @Override @NonNull - private List<Recording> getRecordingsWithState(int state) { - ArrayList<Recording> result = new ArrayList<>(); - for (Recording r : mRecordings.values()) { + protected List<ScheduledRecording> getRecordingsWithState(int state) { + ArrayList<ScheduledRecording> result = new ArrayList<>(); + for (ScheduledRecording r : mScheduledRecordings.values()) { if(r.getState() == state){ result.add(r); } diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java index c62c564b..e3dc622e 100644 --- a/src/com/android/tv/dvr/DvrManager.java +++ b/src/com/android/tv/dvr/DvrManager.java @@ -16,24 +16,33 @@ package com.android.tv.dvr; +import android.content.ContentResolver; import android.content.Context; +import android.media.tv.TvInputInfo; +import android.os.Handler; import android.support.annotation.MainThread; import android.support.annotation.NonNull; +import android.support.annotation.WorkerThread; import android.util.Log; import android.util.Range; +import android.widget.Toast; import com.android.tv.ApplicationSingletons; import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.common.recording.RecordingCapability; +import com.android.tv.common.recording.RecordedProgram; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; -import com.android.tv.util.SoftPreconditions; +import com.android.tv.util.AsyncDbTask; import com.android.tv.util.Utils; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; /** * DVR manager class to add and remove recordings. UI can modify recording list through this class, @@ -45,11 +54,15 @@ public class DvrManager { private final WritableDvrDataManager mDataManager; private final ChannelDataManager mChannelDataManager; private final DvrSessionManager mDvrSessionManager; + // @GuardedBy("mListener") + private final Map<Listener, Handler> mListener = new HashMap<>(); + private final Context mAppContext; public DvrManager(Context context) { SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG); ApplicationSingletons appSingletons = TvApplication.getSingletons(context); mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager(); + mAppContext = context.getApplicationContext(); mChannelDataManager = appSingletons.getChannelDataManager(); mDvrSessionManager = appSingletons.getDvrSessionManger(); } @@ -59,17 +72,18 @@ public class DvrManager { * @param program the program to record * @param recordingsToOverride the possible empty list of recordings that will not be recorded */ - public void addSchedule(Program program, List<Recording> recordingsToOverride) { + public void addSchedule(Program program, List<ScheduledRecording> recordingsToOverride) { Log.i(TAG, "Adding scheduled recording of " + program + " instead of " + recordingsToOverride); - Collections.sort(recordingsToOverride, Recording.PRIORITY_COMPARATOR); + Collections.sort(recordingsToOverride, ScheduledRecording.PRIORITY_COMPARATOR); Channel c = mChannelDataManager.getChannel(program.getChannelId()); long priority = recordingsToOverride.isEmpty() ? Long.MAX_VALUE : recordingsToOverride.get(0).getPriority() - 1; - Recording r = Recording.builder(c, program) + ScheduledRecording r = ScheduledRecording.builder(program) .setPriority(priority) + .setChannelId(c.getId()) .build(); - mDataManager.addRecording(r); + mDataManager.addScheduledRecording(r); } /** @@ -79,8 +93,10 @@ public class DvrManager { Log.i(TAG, "Adding scheduled recording of channel" + channel + " starting at " + Utils.toTimeString(startTime) + " and ending at " + Utils.toTimeString(endTime)); //TODO: handle error cases - Recording r = Recording.builder(channel, startTime, endTime).build(); - mDataManager.addRecording(r); + ScheduledRecording r = ScheduledRecording.builder(startTime, endTime) + .setChannelId(channel.getId()) + .build(); + mDataManager.addScheduledRecording(r); } /** @@ -92,12 +108,45 @@ public class DvrManager { } /** + * Stops the currently recorded program + */ + public void stopRecording(final ScheduledRecording recording) { + synchronized (mListener) { + for (final Entry<Listener, Handler> entry : mListener.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onStopRecordingRequested(recording); + } + }); + } + } + } + + /** * Removes a scheduled recording or an existing recording. */ - public void removeRecording(Recording recording) { - Log.i(TAG, "Removing " + recording); - // TODO(DVR): ask the TIS to delete the recording and respond to the result. - mDataManager.removeRecording(recording); + public void removeScheduledRecording(ScheduledRecording scheduledRecording) { + Log.i(TAG, "Removing " + scheduledRecording); + mDataManager.removeScheduledRecording(scheduledRecording); + } + + public void removeRecordedProgram(final RecordedProgram recordedProgram) { + // TODO(dvr): implement + Log.i(TAG, "To delete " + recordedProgram + + "\nyou should manually delete video data at" + + "\nadb shell rm -rf " + recordedProgram.getDataUri() + ); + Toast.makeText(mAppContext, "Deleting recorded programs is not fully implemented yet", + Toast.LENGTH_SHORT).show(); + new AsyncDbTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + ContentResolver resolver = mAppContext.getContentResolver(); + resolver.delete(recordedProgram.getUri(), null, null); + return null; + } + }.execute(); } /** @@ -107,17 +156,21 @@ public class DvrManager { * <p>Any empty list means there is no conflicts. If there is conflict the program must be * scheduled to record with a Priority lower than the first Recording in the list returned. */ - public List<Recording> getScheduledRecordingsThatConflict(Program program) { + public List<ScheduledRecording> getScheduledRecordingsThatConflict(Program program) { //TODO(DVR): move to scheduler. //TODO(DVR): deal with more than one DvrInputService - List<Recording> overLap = mDataManager.getRecordingsThatOverlapWith(getPeriod(program)); + List<ScheduledRecording> overLap = mDataManager.getRecordingsThatOverlapWith(getPeriod(program)); if (!overLap.isEmpty()) { // TODO(DVR): ignore shows that already won't record. Channel channel = mChannelDataManager.getChannel(program.getChannelId()); if (channel != null) { - RecordingCapability recordingCapability = mDvrSessionManager - .getRecordingCapability(channel.getInputId()); - int remove = Math.max(0, recordingCapability.maxConcurrentTunedSessions - 1); + TvInputInfo info = mDvrSessionManager.getTvInputInfo(channel.getInputId()); + if (info == null) { + Log.w(TAG, + "Could not find a recording TvInputInfo for " + channel.getInputId()); + return overLap; + } + int remove = Math.max(0, info.getTunerCount() - 1); if (remove >= overLap.size()) { return Collections.EMPTY_LIST; } @@ -136,7 +189,7 @@ public class DvrManager { * Checks whether {@code channel} can be tuned without any conflict with existing recordings * in progress. If there is any conflict, {@code outConflictRecordings} will be filled. */ - public boolean canTuneTo(Channel channel, List<Recording> outConflictRecordings) { + public boolean canTuneTo(Channel channel, List<ScheduledRecording> outConflictScheduledRecordings) { // TODO: implement return true; } @@ -145,8 +198,29 @@ public class DvrManager { * Returns true is the inputId supports recording. */ public boolean canRecord(String inputId) { - RecordingCapability recordingCapability = mDvrSessionManager - .getRecordingCapability(inputId); - return recordingCapability != null && recordingCapability.maxConcurrentTunedSessions > 0; + TvInputInfo info = mDvrSessionManager.getTvInputInfo(inputId); + return info != null && info.getTunerCount() > 0; + } + + @WorkerThread + void addListener(Listener listener, @NonNull Handler handler) { + SoftPreconditions.checkNotNull(handler); + synchronized (mListener) { + mListener.put(listener, handler); + } + } + + @WorkerThread + void removeListener(Listener listener) { + synchronized (mListener) { + mListener.remove(listener); + } + } + + /** + * Listener internally used inside dvr package. + */ + interface Listener { + void onStopRecordingRequested(ScheduledRecording scheduledRecording); } } diff --git a/src/com/android/tv/dvr/DvrPlayActivity.java b/src/com/android/tv/dvr/DvrPlayActivity.java index 872e05bd..b117a7cf 100644 --- a/src/com/android/tv/dvr/DvrPlayActivity.java +++ b/src/com/android/tv/dvr/DvrPlayActivity.java @@ -24,7 +24,7 @@ import com.android.tv.R; import com.android.tv.TvApplication; /** - * Simple Activity to play a {@link Recording}. + * Simple Activity to play a {@link ScheduledRecording}. */ public class DvrPlayActivity extends Activity { @@ -35,11 +35,11 @@ public class DvrPlayActivity extends Activity { DvrDataManager dvrDataManager = TvApplication.getSingletons(this).getDvrDataManager(); // TODO(DVR) handle errors. - long recordingId = getIntent().getLongExtra(Recording.RECORDING_ID_EXTRA, 0); - Recording recording = dvrDataManager.getRecording(recordingId); + long recordingId = getIntent().getLongExtra(ScheduledRecording.RECORDING_ID_EXTRA, 0); + ScheduledRecording scheduledRecording = dvrDataManager.getScheduledRecording(recordingId); TextView textView = (TextView) findViewById(R.id.placeHolderText); - if (recording != null) { - textView.setText(recording.toString()); + if (scheduledRecording != null) { + textView.setText(scheduledRecording.toString()); } else { textView.setText(R.string.ut_result_not_found_title); // TODO(DVR) update error text } diff --git a/src/com/android/tv/dvr/DvrRecordingService.java b/src/com/android/tv/dvr/DvrRecordingService.java index d0e86d50..2f3abccf 100644 --- a/src/com/android/tv/dvr/DvrRecordingService.java +++ b/src/com/android/tv/dvr/DvrRecordingService.java @@ -31,7 +31,8 @@ import com.android.tv.ApplicationSingletons; import com.android.tv.TvApplication; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.util.Clock; -import com.android.tv.util.SoftPreconditions; +import com.android.tv.util.RecurringRunner; +import com.android.tv.common.SoftPreconditions; /** * DVR Scheduler service. @@ -57,6 +58,8 @@ public class DvrRecordingService extends Service { context.startService(dvrSchedulerIntent); } + private final Clock mClock = Clock.SYSTEM; + private RecurringRunner mReaperRunner; private WritableDvrDataManager mDataManager; /** @@ -86,14 +89,16 @@ public class DvrRecordingService extends Service { AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); // mScheduler may have been set for testing. if (mScheduler == null) { - DvrSessionManager sessionManager = singletons.getDvrSessionManger(); mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME); mHandlerThread.start(); - mScheduler = new Scheduler(mHandlerThread.getLooper(), sessionManager, mDataManager, - this, Clock.SYSTEM, - alarmManager); + mScheduler = new Scheduler(mHandlerThread.getLooper(), singletons.getDvrManager(), + singletons.getDvrSessionManger(), mDataManager, + singletons.getChannelDataManager(), this, mClock, alarmManager); } - mDataManager.addListener(mScheduler); + mDataManager.addScheduledRecordingListener(mScheduler); + mReaperRunner = new RecurringRunner(this, java.util.concurrent.TimeUnit.DAYS.toMillis(1), + new ScheduledProgramReaper(mDataManager, mClock), null); + mReaperRunner.start(); } @Override @@ -106,7 +111,8 @@ public class DvrRecordingService extends Service { @Override public void onDestroy() { if (DEBUG) Log.d(TAG, "onDestroy"); - mDataManager.removeListener(mScheduler); + mReaperRunner.stop(); + mDataManager.removeScheduledRecordingListener(mScheduler); mScheduler = null; if (mHandlerThread != null) { mHandlerThread.quit(); diff --git a/src/com/android/tv/dvr/DvrSessionManager.java b/src/com/android/tv/dvr/DvrSessionManager.java index 553001e2..fba05cb6 100644 --- a/src/com/android/tv/dvr/DvrSessionManager.java +++ b/src/com/android/tv/dvr/DvrSessionManager.java @@ -16,18 +16,21 @@ package com.android.tv.dvr; -import android.content.ComponentName; +import android.annotation.TargetApi; import android.content.Context; -import android.media.tv.TvContract; +import android.media.tv.TvInputInfo; +import android.media.tv.TvInputManager; +import android.media.tv.TvRecordingClient; +import android.os.Build; +import android.os.Handler; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.support.v4.util.ArrayMap; +import android.util.Log; +import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.common.recording.RecordingCapability; -import com.android.tv.common.recording.TvRecording; import com.android.tv.data.Channel; -import com.android.tv.util.SoftPreconditions; -import com.android.usbtuner.tvinput.UsbTunerTvInputService; /** * Manages Dvr Sessions. @@ -37,57 +40,91 @@ import com.android.usbtuner.tvinput.UsbTunerTvInputService; * <li>Manage capabilities (conflict)</li> * </ul> */ -public class DvrSessionManager { +@TargetApi(Build.VERSION_CODES.N) +public class DvrSessionManager extends TvInputManager.TvInputCallback { + //consider moving all of this to TvInputManagerHelper private final static String TAG = "DvrSessionManager"; + private static final boolean DEBUG = false; + private final Context mContext; - private TvRecording.TvRecordingClient mRecordingClient; - private ArrayMap<String, RecordingCapability> mCapabilityMap = new ArrayMap<>(); + private final TvInputManager mTvInputManager; + private final ArrayMap<String, TvInputInfo> mRecordingTvInputs = new ArrayMap<>(); public DvrSessionManager(Context context) { + this(context, (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE), + new Handler()); + } + + @VisibleForTesting + DvrSessionManager(Context context, TvInputManager tvInputManager, Handler handler) { SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG); + mTvInputManager = tvInputManager; mContext = context.getApplicationContext(); - // TODO(DVR): get a session to all clients, for now just get USB a TestInput - final String inputId = TvContract - .buildInputId(new ComponentName(context, UsbTunerTvInputService.class)); - mRecordingClient = acquireDvrSession(inputId, null); - mRecordingClient.connect(inputId, new TvRecording.ClientCallback() { - @Override - public void onCapabilityReceived(RecordingCapability capability) { - mCapabilityMap.put(inputId, capability); - mRecordingClient.release(); - mRecordingClient = null; + for (TvInputInfo info : tvInputManager.getTvInputList()) { + if (DEBUG) { + Log.d(TAG, info + " canRecord=" + info.canRecord() + " tunerCount=" + info + .getTunerCount()); + } + if (info.canRecord()) { + mRecordingTvInputs.put(info.getId(), info); } - }); - if (CommonFeatures.DVR.isEnabled(context)) { // STOPSHIP(DVR) - String testInputId = "com.android.tv.testinput/.TestTvInputService"; - mCapabilityMap.put(testInputId, - RecordingCapability.builder() - .setInputId(testInputId) - .setMaxConcurrentPlayingSessions(2) - .setMaxConcurrentTunedSessions(2) - .setMaxConcurrentSessionsOfAllTypes(3) - .build()); - } + tvInputManager.registerCallback(this, handler); + } - public TvRecording.TvRecordingClient acquireDvrSession(String inputId, Channel channel) { - // TODO(DVR): use input and channel or change API - TvRecording.TvRecordingClient sessionClient = new TvRecording.TvRecordingClient(mContext); - return sessionClient; + public TvRecordingClient createTvRecordingClient(String tag, + TvRecordingClient.RecordingCallback callback, Handler handler) { + return new TvRecordingClient(mContext, tag, callback, handler); } public boolean canAcquireDvrSession(String inputId, Channel channel) { - // TODO(DVR): implement - return true; + // TODO(DVR): implement checking tuner count etc. + TvInputInfo info = mRecordingTvInputs.get(inputId); + return info != null; + } + + public void releaseTvRecordingClient(TvRecordingClient recordingClient) { + recordingClient.release(); } - public void releaseDvrSession(TvRecording.TvRecordingClient session) { - session.release(); + @Override + public void onInputAdded(String inputId) { + super.onInputAdded(inputId); + TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); + if (DEBUG) { + Log.d(TAG, "onInputAdded " + info.toString() + " canRecord=" + info.canRecord() + + " tunerCount=" + info.getTunerCount()); + } + if (info.canRecord()) { + mRecordingTvInputs.put(inputId, info); + } + } + + @Override + public void onInputRemoved(String inputId) { + super.onInputRemoved(inputId); + if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId); + mRecordingTvInputs.remove(inputId); + } + + @Override + public void onInputUpdated(String inputId) { + super.onInputUpdated(inputId); + TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); + if (DEBUG) { + Log.d(TAG, "onInputUpdated " + info.toString() + " canRecord=" + info.canRecord() + + " tunerCount=" + info.getTunerCount()); + } + if (info.canRecord()) { + mRecordingTvInputs.put(inputId, info); + } else { + mRecordingTvInputs.remove(inputId); + } } @Nullable - public RecordingCapability getRecordingCapability(String inputId) { - return mCapabilityMap.get(inputId); + public TvInputInfo getTvInputInfo(String inputId) { + return mRecordingTvInputs.get(inputId); } } diff --git a/src/com/android/tv/dvr/RecordingTask.java b/src/com/android/tv/dvr/RecordingTask.java index 3bed5e77..804485b3 100644 --- a/src/com/android/tv/dvr/RecordingTask.java +++ b/src/com/android/tv/dvr/RecordingTask.java @@ -16,6 +16,8 @@ package com.android.tv.dvr; +import android.media.tv.TvContract; +import android.media.tv.TvRecordingClient; import android.net.Uri; import android.os.Handler; import android.os.Looper; @@ -24,10 +26,9 @@ import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.util.Log; -import com.android.tv.common.recording.TvRecording; +import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.util.Clock; -import com.android.tv.util.SoftPreconditions; import com.android.tv.util.Utils; import java.util.concurrent.TimeUnit; @@ -39,9 +40,10 @@ import java.util.concurrent.TimeUnit; * There is only one looper so messages must be handled quickly or start a separate thread. */ @WorkerThread -class RecordingTask extends TvRecording.ClientCallback implements Handler.Callback { +class RecordingTask extends TvRecordingClient.RecordingCallback + implements Handler.Callback, DvrManager.Listener { private static final String TAG = "RecordingTask"; - private static final boolean DEBUG = true; //STOPSHIP(DVR) + private static final boolean DEBUG = false; @VisibleForTesting static final int MESSAGE_INIT = 1; @@ -51,11 +53,10 @@ class RecordingTask extends TvRecording.ClientCallback implements Handler.Callba static final int MESSAGE_STOP_RECORDING = 3; @VisibleForTesting - static long MS_BEFORE_START = TimeUnit.SECONDS.toMillis(5); + static final long MS_BEFORE_START = TimeUnit.SECONDS.toMillis(5); @VisibleForTesting - static long MS_AFTER_END = TimeUnit.SECONDS.toMillis(5); + static final long MS_AFTER_END = TimeUnit.SECONDS.toMillis(5); - //STOPSHIP(DVR) don't use enums. @VisibleForTesting enum State { NOT_STARTED, @@ -64,27 +65,33 @@ class RecordingTask extends TvRecording.ClientCallback implements Handler.Callba CONNECTED, RECORDING_START_REQUESTED, RECORDING_STARTED, + RECORDING_STOP_REQUESTED, ERROR, RELEASED, } private final DvrSessionManager mSessionManager; + private final DvrManager mDvrManager; private final WritableDvrDataManager mDataManager; private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); - private TvRecording.TvRecordingClient mSession; + private TvRecordingClient mTvRecordingClient; private Handler mHandler; - private Recording mRecording; + private ScheduledRecording mScheduledRecording; + private final Channel mChannel; private State mState = State.NOT_STARTED; private final Clock mClock; - RecordingTask(Recording recording, DvrSessionManager sessionManager, + RecordingTask(ScheduledRecording scheduledRecording, Channel channel, + DvrManager dvrManager, DvrSessionManager sessionManager, WritableDvrDataManager dataManager, Clock clock) { - mRecording = recording; + mScheduledRecording = scheduledRecording; + mChannel = channel; mSessionManager = sessionManager; mDataManager = dataManager; mClock = clock; + mDvrManager = dvrManager; - if (DEBUG) Log.d(TAG, "created recording task " + mRecording); + if (DEBUG) Log.d(TAG, "created recording task " + mScheduledRecording); } public void setHandler(Handler handler) { @@ -118,60 +125,45 @@ class RecordingTask extends TvRecording.ClientCallback implements Handler.Callba } return true; } catch (Exception e) { - Log.w(TAG, "Error processing message " + msg + " for " + mRecording, e); + Log.w(TAG, "Error processing message " + msg + " for " + mScheduledRecording, e); failAndQuit(); } return false; } @Override - public void onConnected() { - if (DEBUG) Log.d(TAG, "onConnected"); - super.onConnected(); + public void onTuned(Uri channelUri) { + if (DEBUG) { + Log.d(TAG, "onTuned"); + } + super.onTuned(channelUri); mState = State.CONNECTED; + if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_START_RECORDING, + mScheduledRecording.getStartTimeMs() - MS_BEFORE_START)) { + mState = State.ERROR; + return; + } } - @Override - public void onDisconnected() { - if (DEBUG) Log.d(TAG, "onDisconnected"); - super.onDisconnected(); - //Do nothing - } - - @Override - public void onRecordDeleted(Uri mediaUri) { - if (DEBUG) Log.d(TAG, "onRecordDeleted " + mediaUri); - super.onRecordDeleted(mediaUri); - SoftPreconditions.checkState(false, TAG, "unexpected onRecordDeleted"); - - } - - @Override - public void onRecordDeleteFailed(Uri mediaUri, int reason) { - if (DEBUG) Log.d(TAG, "onRecordDeleteFailed " + mediaUri + ", " + reason); - super.onRecordDeleteFailed(mediaUri, reason); - SoftPreconditions.checkState(false, TAG, "unexpected onRecordDeleteFailed"); - } @Override - public void onRecordStarted(Uri mediaUri) { - if (DEBUG) Log.d(TAG, "onRecordStarted " + mediaUri); - super.onRecordStarted(mediaUri); - mState = State.RECORDING_STARTED; - updateRecording(Recording.buildFrom(mRecording) - .setState(Recording.STATE_RECORDING_IN_PROGRESS) - .build()); + public void onRecordingStopped(Uri recordedProgramUri) { + super.onRecordingStopped(recordedProgramUri); + mState = State.CONNECTED; + updateRecording(ScheduledRecording.buildFrom(mScheduledRecording) + .setState(ScheduledRecording.STATE_RECORDING_FINISHED).build()); + sendRemove(); } @Override - public void onRecordStopped(Uri mediaUri, @TvRecording.RecordStopReason int reason) { - if (DEBUG) Log.d(TAG, "onRecordStopped " + mediaUri + " reason " + reason); - super.onRecordStopped(mediaUri, reason); + public void onError(int reason) { + if (DEBUG) Log.d(TAG, "onError reason " + reason); + super.onError(reason); // TODO(dvr) handle success switch (reason) { default: - updateRecording(Recording.buildFrom(mRecording) - .setState(Recording.STATE_RECORDING_FAILED) + updateRecording(ScheduledRecording.buildFrom(mScheduledRecording) + .setState(ScheduledRecording.STATE_RECORDING_FAILED) .build()); } release(); @@ -179,67 +171,78 @@ class RecordingTask extends TvRecording.ClientCallback implements Handler.Callba } private void handleInit() { + if (DEBUG) Log.d(TAG, "handleInit " + mScheduledRecording); //TODO check recording preconditions - Channel channel = mRecording.getChannel(); - if (channel == null) { - Log.w(TAG, "Null channel for " + mRecording); + + if (mScheduledRecording.getEndTimeMs() < mClock.currentTimeMillis()) { + Log.w(TAG, "End time already past, not recording " + mScheduledRecording); failAndQuit(); return; } - String inputId = channel.getInputId(); - if (mSessionManager.canAcquireDvrSession(inputId, channel)) { - mSession = mSessionManager.acquireDvrSession(inputId, channel); - mState = State.SESSION_ACQUIRED; - } else { - Log.w(TAG, "Unable to acquire a session for " + mRecording); + if (mChannel == null) { + Log.w(TAG, "Null channel for " + mScheduledRecording); + failAndQuit(); + return; + } + if (mChannel.getId() != mScheduledRecording.getChannelId()) { + Log.w(TAG, "Channel" + mChannel + " does not match scheduled recording " + + mScheduledRecording); failAndQuit(); return; } - mSession.connect(inputId, this); - mState = State.CONNECTION_PENDING; - - if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_START_RECORDING, - mRecording.getStartTimeMs() - MS_BEFORE_START)) { - mState = State.ERROR; + String inputId = mChannel.getInputId(); + if (mSessionManager.canAcquireDvrSession(inputId, mChannel)) { + mTvRecordingClient = mSessionManager + .createTvRecordingClient("recordingTask-" + mScheduledRecording.getId(), this, + mHandler); + mState = State.SESSION_ACQUIRED; + } else { + Log.w(TAG, "Unable to acquire a session for " + mScheduledRecording); + failAndQuit(); return; } + mDvrManager.addListener(this, mHandler); + mTvRecordingClient.tune(inputId, mChannel.getUri()); + mState = State.CONNECTION_PENDING; } private void failAndQuit() { - updateRecordingState(Recording.STATE_RECORDING_FAILED); + if (DEBUG) Log.d(TAG, "failAndQuit"); + updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED); mState = State.ERROR; sendRemove(); } private void sendRemove() { + if (DEBUG) Log.d(TAG, "sendRemove"); if (mHandler != null) { mHandler.sendEmptyMessage(Scheduler.HandlerWrapper.MESSAGE_REMOVE); } } private void handleStartRecording() { - if (DEBUG)Log.d(TAG, "handleStartRecording " + mRecording); + if (DEBUG) Log.d(TAG, "handleStartRecording " + mScheduledRecording); // TODO(DVR) handle errors - Channel channel = mRecording.getChannel(); - mSession.startRecord(channel.getUri(), getIdAsMediaUri(mRecording)); - mState= State.RECORDING_START_REQUESTED; + long programId = mScheduledRecording.getProgramId(); + mTvRecordingClient.startRecording(programId == ScheduledRecording.ID_NOT_SET ? null + : TvContract.buildProgramUri(programId)); + updateRecording(ScheduledRecording.buildFrom(mScheduledRecording) + .setState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS).build()); + mState = State.RECORDING_STARTED; + if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_STOP_RECORDING, - mRecording.getEndTimeMs() + MS_AFTER_END)) { + mScheduledRecording.getEndTimeMs() + MS_AFTER_END)) { mState = State.ERROR; return; } } private void handleStopRecording() { - if (DEBUG)Log.d(TAG, "handleStopRecording " + mRecording); - mSession.stopRecord(); - // TODO: once we add an API to notify successful completion of recording, - // the following parts need to be moved to the listener implementation. - updateRecording(Recording.buildFrom(mRecording) - .setState(Recording.STATE_RECORDING_FINISHED).build()); - sendRemove(); + if (DEBUG) Log.d(TAG, "handleStopRecording " + mScheduledRecording); + mTvRecordingClient.stopRecording(); + mState = State.RECORDING_STOP_REQUESTED; } @VisibleForTesting @@ -248,10 +251,10 @@ class RecordingTask extends TvRecording.ClientCallback implements Handler.Callba } private void release() { - if (mSession != null) { - mSession.release(); - mSessionManager.releaseDvrSession(mSession); + if (mTvRecordingClient != null) { + mSessionManager.releaseTvRecordingClient(mTvRecordingClient); } + mDvrManager.removeListener(this); } private boolean sendEmptyMessageAtAbsoluteTime(int what, long when) { @@ -264,28 +267,55 @@ class RecordingTask extends TvRecording.ClientCallback implements Handler.Callba return mHandler.sendEmptyMessageDelayed(what, delay); } - private void updateRecordingState(@Recording.RecordingState int state) { - updateRecording(Recording.buildFrom(mRecording).setState(state).build()); + private void updateRecordingState(@ScheduledRecording.RecordingState int state) { + updateRecording(ScheduledRecording.buildFrom(mScheduledRecording).setState(state).build()); } - @VisibleForTesting static Uri getIdAsMediaUri(Recording recording) { + @VisibleForTesting + static Uri getIdAsMediaUri(ScheduledRecording scheduledRecording) { // TODO define the URI format - return new Uri.Builder().appendPath(String.valueOf(recording.getId())).build(); + return new Uri.Builder().appendPath(String.valueOf(scheduledRecording.getId())).build(); } - private void updateRecording(Recording updatedRecording) { - if (DEBUG) Log.d(TAG, "updateRecording " + updatedRecording); - mRecording = updatedRecording; + private void updateRecording(ScheduledRecording updatedScheduledRecording) { + if (DEBUG) Log.d(TAG, "updateScheduledRecording " + updatedScheduledRecording); + mScheduledRecording = updatedScheduledRecording; mMainThreadHandler.post(new Runnable() { @Override public void run() { - mDataManager.updateRecording(mRecording); + mDataManager.updateScheduledRecording(mScheduledRecording); } }); } @Override + public void onStopRecordingRequested(ScheduledRecording recording) { + if (recording.getId() != mScheduledRecording.getId()) { + return; + } + switch (mState) { + case RECORDING_STARTED: + mHandler.removeMessages(MESSAGE_STOP_RECORDING); + handleStopRecording(); + break; + case RECORDING_STOP_REQUESTED: + // Do nothing + break; + case NOT_STARTED: + case SESSION_ACQUIRED: + case CONNECTION_PENDING: + case CONNECTED: + case RECORDING_START_REQUESTED: + case ERROR: + case RELEASED: + default: + sendRemove(); + break; + } + } + + @Override public String toString() { - return getClass().getName() + "(" + mRecording + ")"; + return getClass().getName() + "(" + mScheduledRecording + ")"; } } diff --git a/src/com/android/tv/dvr/ScheduledProgramReaper.java b/src/com/android/tv/dvr/ScheduledProgramReaper.java new file mode 100644 index 00000000..9053eaec --- /dev/null +++ b/src/com/android/tv/dvr/ScheduledProgramReaper.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr; + +import android.support.annotation.MainThread; +import android.support.annotation.VisibleForTesting; + +import com.android.tv.util.Clock; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Deletes {@link ScheduledRecording} older than {@value @DAYS} days. + */ +class ScheduledProgramReaper implements Runnable { + + @VisibleForTesting + static final int DAYS = 2; + private final WritableDvrDataManager mDvrDataManager; + private final Clock mClock; + + ScheduledProgramReaper(WritableDvrDataManager dvrDataManager, Clock clock) { + mDvrDataManager = dvrDataManager; + mClock = clock; + } + + @Override + @MainThread + public void run() { + List<ScheduledRecording> recordings = mDvrDataManager.getAllScheduledRecordings(); + long cutoff = mClock.currentTimeMillis() - TimeUnit.DAYS.toMillis(DAYS); + for (ScheduledRecording r : recordings) { + if (r.getEndTimeMs() < cutoff) { + mDvrDataManager.removeScheduledRecording(r); + } + } + } +} diff --git a/src/com/android/tv/dvr/Recording.java b/src/com/android/tv/dvr/ScheduledRecording.java index 9ecda4da..01b00459 100644 --- a/src/com/android/tv/dvr/Recording.java +++ b/src/com/android/tv/dvr/ScheduledRecording.java @@ -16,49 +16,44 @@ package com.android.tv.dvr; +import android.content.ContentValues; import android.database.Cursor; -import android.net.Uri; import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.Range; +import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.dvr.provider.DvrContract; -import com.android.tv.util.SoftPreconditions; import com.android.tv.util.Utils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; -import java.util.List; /** * A data class for one recording contents. */ @VisibleForTesting -public final class Recording { +public final class ScheduledRecording { private static final String TAG = "Recording"; - public static final String RECORDING_ID_EXTRA = "extra.dvr.recording.id"; + public static final String RECORDING_ID_EXTRA = "extra.dvr.recording.id"; //TODO(DVR) move public static final String PARAM_INPUT_ID = "input_id"; public static final long ID_NOT_SET = -1; - public static final Comparator<Recording> START_TIME_COMPARATOR = new Comparator<Recording>() { + public static final Comparator<ScheduledRecording> START_TIME_COMPARATOR = new Comparator<ScheduledRecording>() { @Override - public int compare(Recording lhs, Recording rhs) { + public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { return Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs); } }; - public static final Comparator<Recording> PRIORITY_COMPARATOR = new Comparator<Recording>() { + public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR = new Comparator<ScheduledRecording>() { @Override - public int compare(Recording lhs, Recording rhs) { + public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { int value = Long.compare(lhs.mPriority, rhs.mPriority); if (value == 0) { value = Long.compare(lhs.mId, rhs.mId); @@ -67,10 +62,10 @@ public final class Recording { } }; - public static final Comparator<Recording> START_TIME_THEN_PRIORITY_COMPARATOR - = new Comparator<Recording>() { + public static final Comparator<ScheduledRecording> START_TIME_THEN_PRIORITY_COMPARATOR + = new Comparator<ScheduledRecording>() { @Override - public int compare(Recording lhs, Recording rhs) { + public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { int value = START_TIME_COMPARATOR.compare(lhs, rhs); if (value == 0) { value = PRIORITY_COMPARATOR.compare(lhs, rhs); @@ -79,18 +74,15 @@ public final class Recording { } }; - public static Builder builder(Channel c, Program p) { + public static Builder builder(Program p) { return new Builder() - .setChannel(c) - .setStartTime(p.getStartTimeUtcMillis()) - .setEndTime(p.getEndTimeUtcMillis()) - .setPrograms(Collections.singletonList(p)) + .setStartTime(p.getStartTimeUtcMillis()).setEndTime(p.getEndTimeUtcMillis()) + .setProgramId(p.getId()) .setType(TYPE_PROGRAM); } - public static Builder builder(Channel c, long startTime, long endTime) { + public static Builder builder(long startTime, long endTime) { return new Builder() - .setChannel(c) .setStartTime(startTime) .setEndTime(endTime) .setType(TYPE_TIMED); @@ -99,13 +91,11 @@ public final class Recording { public static final class Builder { private long mId = ID_NOT_SET; private long mPriority = Long.MAX_VALUE; - private Uri mUri; - private Channel mChannel; - private List<Program> mPrograms; + private long mChannelId; + private long mProgramId = ID_NOT_SET; private @RecordingType int mType; private long mStartTime; private long mEndTime; - private long mSize; private @RecordingState int mState; private SeasonRecording mParentSeasonRecording; @@ -121,18 +111,13 @@ public final class Recording { return this; } - private Builder setUri(Uri uri) { - mUri = uri; + public Builder setChannelId(long channelId) { + mChannelId = channelId; return this; } - private Builder setChannel(Channel channel) { - mChannel = channel; - return this; - } - - public Builder setPrograms(List<Program> programs) { - mPrograms = programs; + public Builder setProgramId(long programId) { + mProgramId = programId; return this; } @@ -151,11 +136,6 @@ public final class Recording { return this; } - public Builder setSize(long size) { - mSize = size; - return this; - } - public Builder setState(@RecordingState int state) { mState = state; return this; @@ -166,28 +146,21 @@ public final class Recording { return this; } - public Recording build() { - return new Recording(mId, mPriority, mUri, mChannel, mPrograms, mType, mStartTime, - mEndTime, mSize, - mState, mParentSeasonRecording); + public ScheduledRecording build() { + return new ScheduledRecording(mId, mPriority, mChannelId, mProgramId, mType, mStartTime, + mEndTime, mState, mParentSeasonRecording); } } /** * Creates {@link Builder} object from the given original {@code Recording}. */ - public static Builder buildFrom(Recording orig) { + public static Builder buildFrom(ScheduledRecording orig) { return new Builder() - .setId(orig.mId) - .setChannel(orig.mChannel) - .setEndTime(orig.mEndTimeMs) - .setParentSeasonRecording(orig.mParentSeasonRecording) - .setPrograms(orig.mPrograms) - .setSize(orig.mMediaSize) - .setStartTime(orig.mStartTimeMs) - .setState(orig.mState) - .setType(orig.mType) - .setUri(orig.mUri); + .setId(orig.mId).setChannelId(orig.mChannelId) + .setEndTime(orig.mEndTimeMs).setParentSeasonRecording(orig.mParentSeasonRecording) + .setProgramId(orig.mProgramId) + .setStartTime(orig.mStartTimeMs).setState(orig.mState).setType(orig.mType); } @Retention(RetentionPolicy.SOURCE) @@ -196,6 +169,7 @@ public final class Recording { public @interface RecordingState {} public static final int STATE_RECORDING_NOT_STARTED = 0; public static final int STATE_RECORDING_IN_PROGRESS = 1; + @Deprecated // It is not used. public static final int STATE_RECORDING_UNEXPECTEDLY_STOPPED = 2; public static final int STATE_RECORDING_FINISHED = 3; public static final int STATE_RECORDING_FAILED = 4; @@ -215,20 +189,46 @@ public final class Recording { @RecordingType private final int mType; /** - * Use this projection if you want to create {@link Recording} object using {@link #fromCursor}. + * Use this projection if you want to create {@link ScheduledRecording} object using {@link #fromCursor}. */ public static final String[] PROJECTION = { - // Columns must match what is read in Recording.fromCursor() - DvrContract.Recordings._ID, - DvrContract.Recordings.COLUMN_PRIORITY, - DvrContract.Recordings.COLUMN_TYPE, - DvrContract.Recordings.COLUMN_URI, - DvrContract.Recordings.COLUMN_CHANNEL_ID, - DvrContract.Recordings.COLUMN_START_TIME_UTC_MILLIS, - DvrContract.Recordings.COLUMN_END_TIME_UTC_MILLIS, - DvrContract.Recordings.COLUMN_MEDIA_SIZE, - DvrContract.Recordings.COLUMN_STATE - }; + // Columns must match what is read in Recording.fromCursor() + DvrContract.Recordings._ID, + DvrContract.Recordings.COLUMN_PRIORITY, + DvrContract.Recordings.COLUMN_TYPE, + DvrContract.Recordings.COLUMN_CHANNEL_ID, + DvrContract.Recordings.COLUMN_PROGRAM_ID, + DvrContract.Recordings.COLUMN_START_TIME_UTC_MILLIS, + DvrContract.Recordings.COLUMN_END_TIME_UTC_MILLIS, + DvrContract.Recordings.COLUMN_STATE}; + /** + * Creates {@link ScheduledRecording} object from the given {@link Cursor}. + */ + public static ScheduledRecording fromCursor(Cursor c) { + int index = -1; + return new Builder() + .setId(c.getLong(++index)) + .setPriority(c.getLong(++index)) + .setType(recordingType(c.getString(++index))) + .setChannelId(c.getLong(++index)) + .setProgramId(c.getLong(++index)) + .setStartTime(c.getLong(++index)) + .setEndTime(c.getLong(++index)) + .setState(recordingState(c.getString(++index))) + .build(); + } + + public static ContentValues toContentValues(ScheduledRecording r) { + ContentValues values = new ContentValues(); + values.put(DvrContract.Recordings.COLUMN_CHANNEL_ID, r.getChannelId()); + values.put(DvrContract.Recordings.COLUMN_PROGRAM_ID, r.getProgramId()); + values.put(DvrContract.Recordings.COLUMN_PRIORITY, r.getPriority()); + values.put(DvrContract.Recordings.COLUMN_START_TIME_UTC_MILLIS, r.getStartTimeMs()); + values.put(DvrContract.Recordings.COLUMN_END_TIME_UTC_MILLIS, r.getEndTimeMs()); + values.put(DvrContract.Recordings.COLUMN_STATE, r.getState()); + values.put(DvrContract.Recordings.COLUMN_TYPE, r.getType()); + return values; + } /** * The ID internal to Live TV @@ -243,53 +243,30 @@ public final class Recording { */ private final long mPriority; - /** - * The {@link Uri} is used as its identifier with the TIS. - * Note: If the state is STATE_RECORDING_NOT_STARTED, this might be {@code null}. - */ - @Nullable - private final Uri mUri; + private final long mChannelId; /** - * Note: mChannel and mPrograms should be loaded from a separate storage not - * from TvProvider, because info from TvProvider can be removed or edited later. - */ - @NonNull - private final Channel mChannel; - /** - * Recorded program info. Its size is usually 1. But, when a channel is recorded by given time - * range, multiple programs can be recorded in one recording. + * Optional id of the associated program. + * */ - @NonNull - private final List<Program> mPrograms; + private final long mProgramId; private final long mStartTimeMs; private final long mEndTimeMs; - private final long mMediaSize; @RecordingState private final int mState; private final SeasonRecording mParentSeasonRecording; - private Recording(long id, long priority, Uri uri, Channel channel, List<Program> programs, - @RecordingType int type, long startTime, long endTime, long size, + private ScheduledRecording(long id, long priority, long channelId, long programId, + @RecordingType int type, long startTime, long endTime, @RecordingState int state, SeasonRecording parentSeasonRecording) { mId = id; mPriority = priority; - if (uri == null && id >= 0 && channel != null) { - uri = new Uri.Builder() - .scheme("record") - .authority("com.android.tv") - .appendPath(Long.toString(mId)) - .appendQueryParameter(PARAM_INPUT_ID, channel.getInputId()) - .build(); - } - mUri = uri; - mChannel = channel; - mPrograms = programs == null ? Collections.EMPTY_LIST : new ArrayList<>(programs); + mChannelId = channelId; + mProgramId = programId; mType = type; mStartTimeMs = startTime; mEndTimeMs = endTime; - mMediaSize = size; mState = state; mParentSeasonRecording = parentSeasonRecording; } @@ -304,24 +281,17 @@ public final class Recording { } /** - * Returns {@link android.net.Uri} representing the recording. - */ - public Uri getUri() { - return mUri; - } - - /** * Returns recorded {@link Channel}. */ - public Channel getChannel() { - return mChannel; + public long getChannelId() { + return mChannelId; } /** - * Returns a list of recorded {@link Program}. + * Return the optional program id */ - public List<Program> getPrograms() { - return mPrograms; + public long getProgramId() { + return mProgramId; } /** @@ -346,13 +316,6 @@ public final class Recording { } /** - * Returns file size which this record consumes. - */ - public long getSize() { - return mMediaSize; - } - - /** * Returns the state. The possible states are {@link #STATE_RECORDING_FINISHED}, * {@link #STATE_RECORDING_IN_PROGRESS} and {@link #STATE_RECORDING_UNEXPECTEDLY_STOPPED}. */ @@ -376,30 +339,6 @@ public final class Recording { } /** - * Creates {@link Recording} object from the given {@link Cursor}. - */ - public static Recording fromCursor(Cursor c, Channel channel, List<Program> programs) { - Builder builder = new Builder(); - int index = -1; - builder.setId(c.getLong(++index)); - builder.setPriority(c.getLong(++index)); - builder.setType(recordingType(c.getString(++index))); - String uri = c.getString(++index); - if (uri != null) { - builder.setUri(Uri.parse(uri)); - } - // Skip channel. - ++index; - builder.setStartTime(c.getLong(++index)); - builder.setEndTime(c.getLong(++index)); - builder.setSize(c.getLong(++index)); - builder.setState(recordingState(c.getString(++index))); - builder.setChannel(channel); - builder.setPrograms(programs); - return builder.build(); - } - - /** * Converts a string to a @RecordingType int, defaulting to {@link #TYPE_TIMED}. */ private static @RecordingType int recordingType(String type) { @@ -459,7 +398,7 @@ public final class Recording { @Override public String toString() { - return "Recording[" + mId + return "ScheduledRecording[" + mId + "]" + "(startTime=" + Utils.toIsoDateTimeString(mStartTimeMs) + ",endTime=" + Utils.toIsoDateTimeString(mEndTimeMs) diff --git a/src/com/android/tv/dvr/Scheduler.java b/src/com/android/tv/dvr/Scheduler.java index 8070f8a6..ff9bde68 100644 --- a/src/com/android/tv/dvr/Scheduler.java +++ b/src/com/android/tv/dvr/Scheduler.java @@ -28,6 +28,8 @@ import android.util.Log; import android.util.LongSparseArray; import android.util.Range; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; import com.android.tv.util.Clock; import java.util.List; @@ -37,7 +39,7 @@ import java.util.concurrent.TimeUnit; * The core class to manage schedule and run actual recording. */ @VisibleForTesting -public class Scheduler implements DvrDataManager.Listener { +public class Scheduler implements DvrDataManager.ScheduledRecordingListener { private static final String TAG = "Scheduler"; private static final boolean DEBUG = false; @@ -51,9 +53,9 @@ public class Scheduler implements DvrDataManager.Listener { public static final int MESSAGE_REMOVE = 999; private final long mId; - HandlerWrapper(Looper looper, Recording recording, RecordingTask recordingTask) { + HandlerWrapper(Looper looper, ScheduledRecording scheduledRecording, RecordingTask recordingTask) { super(looper, recordingTask); - mId = recording.getId(); + mId = scheduledRecording.getId(); } @Override @@ -73,27 +75,32 @@ public class Scheduler implements DvrDataManager.Listener { private final Looper mLooper; private final DvrSessionManager mSessionManager; private final WritableDvrDataManager mDataManager; + private final DvrManager mDvrManager; + private final ChannelDataManager mChannelDataManager; private final Context mContext; private final Clock mClock; private final AlarmManager mAlarmManager; - public Scheduler(Looper looper, DvrSessionManager sessionManager, - WritableDvrDataManager dataManager, Context context, Clock clock, + public Scheduler(Looper looper, DvrManager dvrManager, DvrSessionManager sessionManager, + WritableDvrDataManager dataManager, ChannelDataManager channelDataManager, + Context context, Clock clock, AlarmManager alarmManager) { mLooper = looper; + mDvrManager = dvrManager; mSessionManager = sessionManager; mDataManager = dataManager; + mChannelDataManager = channelDataManager; mContext = context; mClock = clock; mAlarmManager = alarmManager; } private void updatePendingRecordings() { - List<Recording> recordings = mDataManager.getRecordingsThatOverlapWith( + List<ScheduledRecording> scheduledRecordings = mDataManager.getRecordingsThatOverlapWith( new Range(mClock.currentTimeMillis(), mClock.currentTimeMillis() + SOON_DURATION_IN_MS)); // TODO(DVR): handle removing and updating exiting recordings. - for (Recording r : recordings) { + for (ScheduledRecording r : scheduledRecordings) { scheduleRecordingSoon(r); } } @@ -108,18 +115,18 @@ public class Scheduler implements DvrDataManager.Listener { } @Override - public void onRecordingAdded(Recording recording) { - if (DEBUG) Log.d(TAG, "added " + recording); - if (startsWithin(recording, SOON_DURATION_IN_MS)) { - scheduleRecordingSoon(recording); + public void onScheduledRecordingAdded(ScheduledRecording scheduledRecording) { + if (DEBUG) Log.d(TAG, "added " + scheduledRecording); + if (startsWithin(scheduledRecording, SOON_DURATION_IN_MS)) { + scheduleRecordingSoon(scheduledRecording); } else { updateNextAlarm(); } } @Override - public void onRecordingRemoved(Recording recording) { - long id = recording.getId(); + public void onScheduledRecordingRemoved(ScheduledRecording scheduledRecording) { + long id = scheduledRecording.getId(); HandlerWrapper wrapper = mPendingRecordings.get(id); if (wrapper != null) { wrapper.removeCallbacksAndMessages(null); @@ -130,16 +137,18 @@ public class Scheduler implements DvrDataManager.Listener { } @Override - public void onRecordingStatusChanged(Recording recording) { + public void onScheduledRecordingStatusChanged(ScheduledRecording scheduledRecording) { //TODO(DVR): implement } - private void scheduleRecordingSoon(Recording recording) { - RecordingTask recordingTask = new RecordingTask(recording, mSessionManager, mDataManager, - mClock); - HandlerWrapper handlerWrapper = new HandlerWrapper(mLooper, recording, recordingTask); + private void scheduleRecordingSoon(ScheduledRecording scheduledRecording) { + Channel channel = mChannelDataManager.getChannel(scheduledRecording.getChannelId()); + RecordingTask recordingTask = new RecordingTask(scheduledRecording, channel, mDvrManager, + mSessionManager, mDataManager, mClock); + HandlerWrapper handlerWrapper = new HandlerWrapper(mLooper, scheduledRecording, + recordingTask); recordingTask.setHandler(handlerWrapper); - mPendingRecordings.put(recording.getId(), handlerWrapper); + mPendingRecordings.put(scheduledRecording.getId(), handlerWrapper); handlerWrapper.sendEmptyMessage(RecordingTask.MESSAGE_INIT); } @@ -164,7 +173,7 @@ public class Scheduler implements DvrDataManager.Listener { } @VisibleForTesting - boolean startsWithin(Recording recording, long durationInMs) { - return mClock.currentTimeMillis() >= recording.getStartTimeMs() - durationInMs; + boolean startsWithin(ScheduledRecording scheduledRecording, long durationInMs) { + return mClock.currentTimeMillis() >= scheduledRecording.getStartTimeMs() - durationInMs; } } diff --git a/src/com/android/tv/dvr/SeasonRecording.java b/src/com/android/tv/dvr/SeasonRecording.java index 074ef017..7f89e135 100644 --- a/src/com/android/tv/dvr/SeasonRecording.java +++ b/src/com/android/tv/dvr/SeasonRecording.java @@ -29,7 +29,7 @@ public class SeasonRecording { */ private static final int ALL_SEASON = -1; - private List<Recording> mSchedule; + private List<ScheduledRecording> mSchedule; private String mTitle; private int mSeasonNumber; } diff --git a/src/com/android/tv/dvr/WritableDvrDataManager.java b/src/com/android/tv/dvr/WritableDvrDataManager.java index 87809701..0b8a4c99 100644 --- a/src/com/android/tv/dvr/WritableDvrDataManager.java +++ b/src/com/android/tv/dvr/WritableDvrDataManager.java @@ -29,7 +29,7 @@ interface WritableDvrDataManager extends DvrDataManager { /** * Add a new recording. */ - void addRecording(Recording recording); + void addScheduledRecording(ScheduledRecording scheduledRecording); /** * Add a season recording/ @@ -39,7 +39,7 @@ interface WritableDvrDataManager extends DvrDataManager { /** * Remove a recording. */ - void removeRecording(Recording Recording); + void removeScheduledRecording(ScheduledRecording ScheduledRecording); /** * Remove a season schedule. @@ -49,5 +49,5 @@ interface WritableDvrDataManager extends DvrDataManager { /** * Update an existing recording. */ - void updateRecording(Recording r); + void updateScheduledRecording(ScheduledRecording r); } diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java index 3fc6e4a9..6058aa54 100644 --- a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java +++ b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java @@ -21,19 +21,12 @@ import android.database.Cursor; import android.os.AsyncTask; import android.support.annotation.Nullable; -import com.android.tv.data.Channel; -import com.android.tv.data.Program; -import com.android.tv.dvr.Recording; -import com.android.tv.dvr.provider.DvrContract.DvrChannels; -import com.android.tv.dvr.provider.DvrContract.DvrPrograms; -import com.android.tv.dvr.provider.DvrContract.RecordingToPrograms; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.provider.DvrContract.Recordings; import com.android.tv.util.NamedThreadFactory; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -87,14 +80,14 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result> * The id will be -1 if there was an error. */ public abstract static class AsyncAddRecordingTask - extends AsyncDvrDbTask<Recording, Void, List<Recording>> { + extends AsyncDvrDbTask<ScheduledRecording, Void, List<ScheduledRecording>> { public AsyncAddRecordingTask(Context context) { super(context); } @Override - protected final List<Recording> doInDvrBackground(Recording... params) { + protected final List<ScheduledRecording> doInDvrBackground(ScheduledRecording... params) { return sDbHelper.insertRecordings(params); } } @@ -106,13 +99,13 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result> * if no match was found. The count is expected to be exactly 1 for each recording. */ public abstract static class AsyncUpdateRecordingTask - extends AsyncDvrDbTask<Recording, Void, List<Integer>> { + extends AsyncDvrDbTask<ScheduledRecording, Void, List<Integer>> { public AsyncUpdateRecordingTask(Context context) { super(context); } @Override - protected final List<Integer> doInDvrBackground(Recording... params) { + protected final List<Integer> doInDvrBackground(ScheduledRecording... params) { return sDbHelper.updateRecordings(params); } } @@ -124,91 +117,43 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result> * if no match was found. The count is expected to be exactly 1 for each recording. */ public abstract static class AsyncDeleteRecordingTask - extends AsyncDvrDbTask<Recording, Void, List<Integer>> { + extends AsyncDvrDbTask<ScheduledRecording, Void, List<Integer>> { public AsyncDeleteRecordingTask(Context context) { super(context); } @Override - protected final List<Integer> doInDvrBackground(Recording... params) { + protected final List<Integer> doInDvrBackground(ScheduledRecording... params) { return sDbHelper.deleteRecordings(params); } } public abstract static class AsyncDvrQueryTask - extends AsyncDvrDbTask<Void, Void, List<Recording>> { + extends AsyncDvrDbTask<Void, Void, List<ScheduledRecording>> { public AsyncDvrQueryTask(Context context) { super(context); } @Override @Nullable - protected final List<Recording> doInDvrBackground(Void... params) { + protected final List<ScheduledRecording> doInDvrBackground(Void... params) { if (isCancelled()) { return null; } - // Read Channels Table. - Map<Long, Channel> channelMap = new HashMap<>(); - try (Cursor c = sDbHelper.query(DvrChannels.TABLE_NAME, Channel.PROJECTION_DVR)) { - while (c.moveToNext() && !isCancelled()) { - Channel channel = Channel.fromDvrCursor(c); - channelMap.put(channel.getDvrId(), channel); - } - } - - if (isCancelled()) { - return null; - } - // Read Programs Table. - Map<Long, Program> programMap = new HashMap<>(); - try (Cursor c = sDbHelper.query(DvrPrograms.TABLE_NAME, Program.PROJECTION_DVR)) { - while (c.moveToNext() && !isCancelled()) { - Program program = Program.fromDvrCursor(c); - programMap.put(program.getDvrId(), program); - } - } if (isCancelled()) { return null; } - // Read Mapping Table. - Map<Long, List<Long>> recordingToProgramMap = new HashMap<>(); - try (Cursor c = sDbHelper.query(RecordingToPrograms.TABLE_NAME, new String[] { - RecordingToPrograms.COLUMN_RECORDING_ID, - RecordingToPrograms.COLUMN_PROGRAM_ID})) { - while (c.moveToNext() && !isCancelled()) { - long recordingId = c.getLong(0); - List<Long> programList = recordingToProgramMap.get(recordingId); - if (programList == null) { - programList = new ArrayList<>(); - recordingToProgramMap.put(recordingId, programList); - } - programList.add(c.getLong(1)); - } - } - if (isCancelled()) { return null; } - List<Recording> recordings = new ArrayList<>(); - try (Cursor c = sDbHelper.query(Recordings.TABLE_NAME, Recording.PROJECTION)) { - int idIndex = c.getColumnIndex(Recordings._ID); - int channelIndex = c.getColumnIndex(Recordings.COLUMN_CHANNEL_ID); + List<ScheduledRecording> scheduledRecordings = new ArrayList<>(); + try (Cursor c = sDbHelper.query(Recordings.TABLE_NAME, ScheduledRecording.PROJECTION)) { while (c.moveToNext() && !isCancelled()) { - Channel channel = channelMap.get(c.getLong(channelIndex)); - List<Program> programs = null; - long recordingId = c.getLong(idIndex); - List<Long> programIds = recordingToProgramMap.get(recordingId); - if (programIds != null) { - programs = new ArrayList<>(); - for (long programId : programIds) { - programs.add(programMap.get(programId)); - } - } - recordings.add(Recording.fromCursor(c, channel, programs)); + scheduledRecordings.add(ScheduledRecording.fromCursor(c)); } } - return recordings; + return scheduledRecordings; } } } diff --git a/src/com/android/tv/dvr/provider/DvrContract.java b/src/com/android/tv/dvr/provider/DvrContract.java index e6ce4141..192cc17b 100644 --- a/src/com/android/tv/dvr/provider/DvrContract.java +++ b/src/com/android/tv/dvr/provider/DvrContract.java @@ -73,22 +73,23 @@ public final class DvrContract { public static final String COLUMN_TYPE = "type"; /** - * The URI string for the recorded media. + * The ID of the channel for recording. * - * <p>This field can be null if the media is not recorded yet. + * <p>This is a required field. * - * <p>Type: String + * <p>Type: INTEGER (long) */ - public static final String COLUMN_URI = "uri"; + public static final String COLUMN_CHANNEL_ID = "channel_id"; + /** - * The ID of the channel for recording. + * The ID of the associated program for recording. * - * <p>This is a required field. It's not an ID in TvProvider, but in DVR database. + * <p>This is an optional field. * * <p>Type: INTEGER (long) */ - public static final String COLUMN_CHANNEL_ID = "channel_id"; + public static final String COLUMN_PROGRAM_ID = "program_id"; /** * The start time of this recording, in milliseconds since the epoch. @@ -109,13 +110,6 @@ public final class DvrContract { public static final String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis"; /** - * The size of the stored media in bytes. - * - * <p>Type: INTEGER (long) - */ - public static final String COLUMN_MEDIA_SIZE = "media_size"; - - /** * The state of this recording. * * <p>This value should be one of the followings: {@link #STATE_RECORDING_NOT_STARTED}, @@ -127,49 +121,9 @@ public final class DvrContract { * <p>Type: String */ public static final String COLUMN_STATE = "state"; - } - /** - * Column definition for channels for recording. - * - * <p>This is the subset of {@link android.media.tv.TvContract.Channels}. - */ - public static final class DvrChannels implements BaseColumns { - /** The table name. */ - public static final String TABLE_NAME = "dvr_channels"; + private Recordings() { } } - /** - * Column definition for programs for recording. - * - * <p>This is the subset of {@link android.media.tv.TvContract.Programs}. - */ - public static final class DvrPrograms implements BaseColumns { - /** The table name. */ - public static final String TABLE_NAME = "dvr_programs"; - } - - /** Column definition for the mapping from recording to programs */ - public static final class RecordingToPrograms implements BaseColumns { - /** The table name. */ - public static final String TABLE_NAME = "recording_to_programs"; - - /** - * The ID of the recording. - * - * <p>This is a required field. - * - * <p>Type: INTEGER (long) - */ - public static final String COLUMN_RECORDING_ID = "recording_id"; - - /** - * The ID of the program. - * - * <p>This is a required field. It's not an ID in TvProvider, but in DVR database. - * - * <p>Type: INTEGER (long) - */ - public static final String COLUMN_PROGRAM_ID = "program_id"; - } + private DvrContract() { } } diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java index 2445e935..bdba8ac3 100644 --- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java +++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java @@ -24,11 +24,7 @@ import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.util.Log; -import com.android.tv.data.Channel; -import com.android.tv.dvr.Recording; -import com.android.tv.dvr.provider.DvrContract.DvrChannels; -import com.android.tv.dvr.provider.DvrContract.DvrPrograms; -import com.android.tv.dvr.provider.DvrContract.RecordingToPrograms; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.provider.DvrContract.Recordings; import java.util.ArrayList; @@ -41,49 +37,22 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { private static final String TAG = "DvrDatabaseHelper"; private static final boolean DEBUG = true; - private static final int DATABASE_VERSION = 2; + private static final int DATABASE_VERSION = 4; private static final String DB_NAME = "dvr.db"; private static final String SQL_CREATE_RECORDINGS = "CREATE TABLE " + Recordings.TABLE_NAME + "(" - + Recordings._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + Recordings.COLUMN_PRIORITY - + " INTEGER DEFAULT " + Long.MAX_VALUE + "," + + Recordings._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Recordings.COLUMN_PRIORITY + " INTEGER DEFAULT " + Long.MAX_VALUE + "," + Recordings.COLUMN_TYPE + " TEXT NOT NULL," - + Recordings.COLUMN_URI + " TEXT," + Recordings.COLUMN_CHANNEL_ID + " INTEGER NOT NULL," + + Recordings.COLUMN_PROGRAM_ID + " INTEGER ," + Recordings.COLUMN_START_TIME_UTC_MILLIS + " INTEGER NOT NULL," + Recordings.COLUMN_END_TIME_UTC_MILLIS + " INTEGER NOT NULL," - + Recordings.COLUMN_MEDIA_SIZE + " INTEGER," + Recordings.COLUMN_STATE + " TEXT NOT NULL)"; - private static final String SQL_CREATE_DVR_CHANNELS = - "CREATE TABLE " + DvrChannels.TABLE_NAME + "(" - + DvrChannels._ID + " INTEGER PRIMARY KEY AUTOINCREMENT)"; - - private static final String SQL_CREATE_DVR_PROGRAMS = - "CREATE TABLE " + DvrPrograms.TABLE_NAME + "(" - + DvrPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT)"; - - private static final String SQL_CREATE_RECORDING_PROGRAMS = - "CREATE TABLE " + RecordingToPrograms.TABLE_NAME + "(" - + RecordingToPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," - + RecordingToPrograms.COLUMN_RECORDING_ID + " INTEGER," - + RecordingToPrograms.COLUMN_PROGRAM_ID + " INTEGER," - + "FOREIGN KEY(" + RecordingToPrograms.COLUMN_RECORDING_ID - + ") REFERENCES " + Recordings.TABLE_NAME + "(" + Recordings._ID - + ") ON UPDATE CASCADE ON DELETE CASCADE," - + "FOREIGN KEY(" + RecordingToPrograms.COLUMN_PROGRAM_ID - + ") REFERENCES " + DvrPrograms.TABLE_NAME + "(" + DvrPrograms._ID - + ") ON UPDATE CASCADE ON DELETE CASCADE)"; - private static final String SQL_DROP_RECORDINGS = "DROP TABLE IF EXISTS " + Recordings.TABLE_NAME; - private static final String SQL_DROP_DVR_CHANNELS = "DROP TABLE IF EXISTS " - + DvrChannels.TABLE_NAME; - private static final String SQL_DROP_DVR_PROGRAMS = "DROP TABLE IF EXISTS " - + DvrPrograms.TABLE_NAME; - private static final String SQL_DROP_RECORDING_PROGRAMS = "DROP TABLE IF EXISTS " - + RecordingToPrograms.TABLE_NAME; public static final String WHERE_RECORDING_ID_EQUALS = Recordings._ID + " = ?"; public DvrDatabaseHelper(Context context) { @@ -99,22 +68,10 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { public void onCreate(SQLiteDatabase db) { if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_RECORDINGS); db.execSQL(SQL_CREATE_RECORDINGS); - if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_DVR_CHANNELS); - db.execSQL(SQL_CREATE_DVR_CHANNELS); - if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_DVR_PROGRAMS); - db.execSQL(SQL_CREATE_DVR_PROGRAMS); - if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_RECORDING_PROGRAMS); - db.execSQL(SQL_CREATE_RECORDING_PROGRAMS); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_RECORDING_PROGRAMS); - db.execSQL(SQL_DROP_RECORDING_PROGRAMS); - if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_DVR_PROGRAMS); - db.execSQL(SQL_DROP_DVR_PROGRAMS); - if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_DVR_CHANNELS); - db.execSQL(SQL_DROP_DVR_CHANNELS); if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_RECORDINGS); db.execSQL(SQL_DROP_RECORDINGS); onCreate(db); @@ -135,15 +92,15 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { * * @return The list of recordings with id set. The id will be -1 if there was an error. */ - public List<Recording> insertRecordings(Recording... recordings) { - updateChannelsFromRecordings(recordings); + public List<ScheduledRecording> insertRecordings(ScheduledRecording... scheduledRecordings) { + updateChannelsFromRecordings(scheduledRecordings); SQLiteDatabase db = getReadableDatabase(); - List<Recording> results = new ArrayList<>(); - for (Recording r : recordings) { - ContentValues values = getContentValues(r); + List<ScheduledRecording> results = new ArrayList<>(); + for (ScheduledRecording r : scheduledRecordings) { + ContentValues values = ScheduledRecording.toContentValues(r); long id = db.insert(Recordings.TABLE_NAME, null, values); - results.add(Recording.buildFrom(r).setId(id).build()); + results.add(ScheduledRecording.buildFrom(r).setId(id).build()); } return results; } @@ -154,13 +111,12 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { * @return The list of row update counts. The count will be -1 if there was an error or 0 * if no match was found. The count is expected to be exactly 1 for each recording. */ - public List<Integer> updateRecordings(Recording[] recordings) { - updateChannelsFromRecordings(recordings); + public List<Integer> updateRecordings(ScheduledRecording[] scheduledRecordings) { + updateChannelsFromRecordings(scheduledRecordings); SQLiteDatabase db = getWritableDatabase(); List<Integer> results = new ArrayList<>(); - long count = 0; - for (Recording r : recordings) { - ContentValues values = getContentValues(r); + for (ScheduledRecording r : scheduledRecordings) { + ContentValues values = ScheduledRecording.toContentValues(r); int updated = db.update(Recordings.TABLE_NAME, values, Recordings._ID + " = ?", new String[] {String.valueOf(r.getId())}); results.add(updated); @@ -168,42 +124,21 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { return results; } - private void updateChannelsFromRecordings(Recording[] recordings) { + private void updateChannelsFromRecordings(ScheduledRecording[] scheduledRecordings) { // TODO(DVR) implement/ // TODO(DVR) consider not deleting channels instead of keeping a separate table. } - private ContentValues getContentValues(Recording r) { - ContentValues values = new ContentValues(); - // TODO(DVR): use DVR channel id instead - Channel channel = r.getChannel(); - if (channel != null) { - values.put(Recordings.COLUMN_CHANNEL_ID, channel.getId()); - } - values.put(Recordings.COLUMN_PRIORITY, r.getPriority()); - values.put(Recordings.COLUMN_START_TIME_UTC_MILLIS, r.getStartTimeMs()); - values.put(Recordings.COLUMN_END_TIME_UTC_MILLIS, r.getEndTimeMs()); - values.put(Recordings.COLUMN_STATE, r.getState()); - values.put(Recordings.COLUMN_MEDIA_SIZE, r.getSize()); - values.put(Recordings.COLUMN_TYPE, r.getType()); - if (r.getUri() != null) { - values.put(Recordings.COLUMN_URI, r.getUri().toString()); - } - return values; - } - /** * Delete recordings. * * @return The list of row update counts. The count will be -1 if there was an error or 0 * if no match was found. The count is expected to be exactly 1 for each recording. */ - public List<Integer> deleteRecordings(Recording[] recordings) { + public List<Integer> deleteRecordings(ScheduledRecording[] scheduledRecordings) { SQLiteDatabase db = getWritableDatabase(); List<Integer> results = new ArrayList<>(); - long count = 0; - for (Recording r : recordings) { - ContentValues values = getContentValues(r); + for (ScheduledRecording r : scheduledRecordings) { int deleted = db.delete(Recordings.TABLE_NAME, WHERE_RECORDING_ID_EQUALS, new String[] {String.valueOf(r.getId())}); results.add(deleted); diff --git a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java index 87e47930..70e71cab 100644 --- a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java +++ b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java @@ -20,26 +20,33 @@ import android.os.Bundle; import android.support.annotation.IntDef; import android.support.v17.leanback.app.BrowseFragment; import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.ClassPresenterSelector; import android.support.v17.leanback.widget.HeaderItem; import android.support.v17.leanback.widget.ListRow; import android.support.v17.leanback.widget.ListRowPresenter; +import android.support.v17.leanback.widget.ObjectAdapter; import android.util.Log; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.common.recording.RecordedProgram; import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.Recording; +import com.android.tv.dvr.ScheduledRecording; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.LinkedHashMap; -import java.util.List; /** * {@link BrowseFragment} for DVR functions. */ public class DvrBrowseFragment extends BrowseFragment { private static final String TAG = "DvrBrowseFragment"; + private static final boolean DEBUG = false; + + private ScheduledRecordingsAdapter mRecordingsInProgressAdapter; + private ScheduledRecordingsAdapter mRecordingsNotStatedAdapter; + private RecordedProgramsAdapter mRecordedProgramsAdapter; @IntDef({DVR_CURRENT_RECORDINGS, DVR_SCHEDULED_RECORDINGS, DVR_RECORDED_PROGRAMS, DVR_SETTINGS}) @Retention(RetentionPolicy.SOURCE) @@ -49,27 +56,48 @@ public class DvrBrowseFragment extends BrowseFragment { public static final int DVR_RECORDED_PROGRAMS = 2; public static final int DVR_SETTINGS = 3; - private static LinkedHashMap<Integer, Integer> sHeaders = + private static final LinkedHashMap<Integer, Integer> sHeaders = new LinkedHashMap<Integer, Integer>() {{ put(DVR_CURRENT_RECORDINGS, R.string.dvr_main_current_recordings); put(DVR_SCHEDULED_RECORDINGS, R.string.dvr_main_scheduled_recordings); put(DVR_RECORDED_PROGRAMS, R.string.dvr_main_recorded_programs); - put(DVR_SETTINGS, R.string.dvr_main_settings); + /* put(DVR_SETTINGS, R.string.dvr_main_settings); */ // TODO: Temporarily remove it for DP. }}; private DvrDataManager mDvrDataManager; private ArrayObjectAdapter mRowsAdapter; @Override - public void onActivityCreated(Bundle savedInstanceState) { - Log.d(TAG, "onCreate"); - super.onActivityCreated(savedInstanceState); + public void onCreate(Bundle savedInstanceState) { + if (DEBUG) Log.d(TAG, "onCreate"); + super.onCreate(savedInstanceState); + mDvrDataManager = TvApplication.getSingletons(getContext()).getDvrDataManager(); setupUiElements(); - setupAdapter(); + setupAdapters(); + mRecordingsInProgressAdapter.start(); + mRecordingsNotStatedAdapter.start(); + mRecordedProgramsAdapter.start(); + initRows(); prepareEntranceTransition(); + startEntranceTransition(); + } - // TODO: load asynchronously. - loadData(); + @Override + public void onStart() { + if (DEBUG) Log.d(TAG, "onStart"); + super.onStart(); + // TODO: It's a workaround for a bug that a progress bar isn't hidden. + // We need to remove it later. + getProgressBarManager().disableProgressBar(); + } + + @Override + public void onDestroy() { + if (DEBUG) Log.d(TAG, "onDestroy"); + mRecordingsInProgressAdapter.stop(); + mRecordingsNotStatedAdapter.stop(); + mRecordedProgramsAdapter.stop(); + super.onDestroy(); } private void setupUiElements() { @@ -77,43 +105,51 @@ public class DvrBrowseFragment extends BrowseFragment { setHeadersTransitionOnBackEnabled(false); } - private void setupAdapter() { - mDvrDataManager = TvApplication.getSingletons(getContext()).getDvrDataManager(); + private void setupAdapters() { mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); setAdapter(mRowsAdapter); + ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); + EmptyItemPresenter emptyItemPresenter = new EmptyItemPresenter(this); + ScheduledRecordingPresenter scheduledRecordingPresenter = new ScheduledRecordingPresenter( + getContext()); + RecordedProgramPresenter recordedProgramPresenter = new RecordedProgramPresenter( + getContext()); + presenterSelector.addClassPresenter(ScheduledRecording.class, scheduledRecordingPresenter); + presenterSelector.addClassPresenter(RecordedProgram.class, recordedProgramPresenter); + presenterSelector.addClassPresenter(EmptyHolder.class, emptyItemPresenter); + mRecordingsInProgressAdapter = new ScheduledRecordingsAdapter(mDvrDataManager, + ScheduledRecording.STATE_RECORDING_IN_PROGRESS, presenterSelector); + mRecordingsNotStatedAdapter = new ScheduledRecordingsAdapter(mDvrDataManager, + ScheduledRecording.STATE_RECORDING_NOT_STARTED, presenterSelector); + mRecordedProgramsAdapter = new RecordedProgramsAdapter(mDvrDataManager, presenterSelector); } - private void loadRow(ArrayObjectAdapter gridRowAdapter, List<Recording> recordings) { - if (recordings == null || recordings.size() == 0) { - gridRowAdapter.add(null); - return; - } - for (Recording r : recordings) { - gridRowAdapter.add(r); - } - } - - private void loadData() { + private void initRows() { + mRowsAdapter.clear(); for (@DVR_HEADERS_MODE int i : sHeaders.keySet()) { HeaderItem gridHeader = new HeaderItem(i, getContext().getString(sHeaders.get(i))); - GridItemPresenter gridPresenter = new GridItemPresenter(this); - ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(gridPresenter); + ObjectAdapter gridRowAdapter = null; switch (i) { - case DVR_CURRENT_RECORDINGS: - loadRow(gridRowAdapter, mDvrDataManager.getStartedRecordings()); + case DVR_CURRENT_RECORDINGS: { + gridRowAdapter = mRecordingsInProgressAdapter; break; - case DVR_SCHEDULED_RECORDINGS: - loadRow(gridRowAdapter, mDvrDataManager.getScheduledRecordings()); + } + case DVR_SCHEDULED_RECORDINGS: { + gridRowAdapter = mRecordingsNotStatedAdapter; + } break; - case DVR_RECORDED_PROGRAMS: - loadRow(gridRowAdapter, mDvrDataManager.getFinishedRecordings()); + case DVR_RECORDED_PROGRAMS: { + gridRowAdapter = mRecordedProgramsAdapter; + } break; case DVR_SETTINGS: + gridRowAdapter = new ArrayObjectAdapter(new EmptyItemPresenter(this)); // TODO: provide setup rows. break; } - mRowsAdapter.add(new ListRow(gridHeader, gridRowAdapter)); + if (gridRowAdapter != null) { + mRowsAdapter.add(new ListRow(gridHeader, gridRowAdapter)); + } } - startEntranceTransition(); } } diff --git a/src/com/android/tv/dvr/ui/DvrDialogFragment.java b/src/com/android/tv/dvr/ui/DvrDialogFragment.java new file mode 100644 index 00000000..38de9d8d --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrDialogFragment.java @@ -0,0 +1,50 @@ +package com.android.tv.dvr.ui; + +import android.app.FragmentManager; +import android.content.Context; +import android.os.Bundle; +import android.support.v17.leanback.app.GuidedStepFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.MainActivity; +import com.android.tv.R; +import com.android.tv.guide.ProgramGuide; + +public class DvrDialogFragment extends HalfSizedDialogFragment { + private final DvrGuidedStepFragment mDvrGuidedStepFragment; + + public DvrDialogFragment(DvrGuidedStepFragment dvrGuidedStepFragment) { + mDvrGuidedStepFragment = dvrGuidedStepFragment; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + ProgramGuide programGuide = + ((MainActivity) getActivity()).getOverlayManager().getProgramGuide(); + if (programGuide != null && programGuide.isActive()) { + programGuide.cancelHide(); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + FragmentManager fm = getChildFragmentManager(); + GuidedStepFragment.add(fm, mDvrGuidedStepFragment, R.id.halfsized_dialog_host); + return view; + } + + @Override + public void onDetach() { + super.onDetach(); + ProgramGuide programGuide = + ((MainActivity) getActivity()).getOverlayManager().getProgramGuide(); + if (programGuide != null && programGuide.isActive()) { + programGuide.scheduleHide(); + } + } +} diff --git a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java new file mode 100644 index 00000000..0854b91a --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java @@ -0,0 +1,73 @@ +package com.android.tv.dvr.ui; + +import android.content.Context; +import android.os.Bundle; +import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidanceStylist; +import android.support.v17.leanback.widget.VerticalGridView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.MainActivity; +import com.android.tv.TvApplication; +import com.android.tv.dialog.SafeDismissDialogFragment; +import com.android.tv.dvr.DvrManager; +import com.android.tv.guide.ProgramManager.TableEntry; +import com.android.tv.R; + +public class DvrGuidedStepFragment extends GuidedStepFragment { + private final TableEntry mEntry; + private DvrManager mDvrManager; + + public DvrGuidedStepFragment(TableEntry entry) { + mEntry = entry; + } + + protected TableEntry getEntry() { + return mEntry; + } + + protected DvrManager getDvrManager() { + return mDvrManager; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mDvrManager = TvApplication.getSingletons(context).getDvrManager(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + VerticalGridView gridView = getGuidedActionsStylist().getActionsGridView(); + gridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE); + return view; + } + + @Override + public GuidanceStylist onCreateGuidanceStylist() { + // Workaround: b/28448653 + return new GuidanceStylist() { + @Override + public int onProvideLayoutId() { + return R.layout.halfsized_guidance; + } + }; + } + + @Override + public int onProvideTheme() { + return R.style.Theme_TV_Dvr_GuidedStep; + } + + protected void dismissDialog() { + SafeDismissDialogFragment currentDialog = + ((MainActivity) getActivity()).getOverlayManager().getCurrentDialog(); + if (currentDialog instanceof DvrDialogFragment) { + currentDialog.dismiss(); + } + } +} diff --git a/src/com/android/tv/dvr/ui/DvrRecordConflictFragment.java b/src/com/android/tv/dvr/ui/DvrRecordConflictFragment.java new file mode 100644 index 00000000..92052b5b --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrRecordConflictFragment.java @@ -0,0 +1,82 @@ +package com.android.tv.dvr.ui; + +import android.app.Activity; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.tv.MainActivity; +import com.android.tv.R; + +import android.support.v17.leanback.widget.GuidanceStylist.Guidance; +import android.support.v17.leanback.widget.GuidedAction; + +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.Program; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.guide.ProgramManager.TableEntry; + +import java.text.DateFormat; +import java.util.Date; +import java.util.List; + +public class DvrRecordConflictFragment extends DvrGuidedStepFragment { + private static final int DVR_EPG_RECORD = 1; + private static final int DVR_EPG_NOT_RECORD = 2; + + private List<ScheduledRecording> mConflicts; + + public DvrRecordConflictFragment(TableEntry entry) { + super(entry); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + mConflicts = getDvrManager().getScheduledRecordingsThatConflict(getEntry().program); + return super.onCreateView(inflater, container, savedInstanceState); + } + + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + final MainActivity tvActivity = (MainActivity) getActivity(); + final ChannelDataManager channelDataManager = tvActivity.getChannelDataManager(); + StringBuilder sb = new StringBuilder(); + for (ScheduledRecording r : mConflicts) { + Channel channel = channelDataManager.getChannel(r.getChannelId()); + if (channel == null) { + continue; + } + sb.append(channel.getDisplayName()) + .append(" : ") + .append(DateFormat.getDateTimeInstance().format(new Date(r.getStartTimeMs()))) + .append("\n"); + } + String title = getResources().getString(R.string.dvr_epg_conflict_dialog_title); + String description = sb.toString(); + return new Guidance(title, description, null, null); + } + + @Override + public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { + Activity activity = getActivity(); + actions.add(new GuidedAction.Builder(activity) + .id(DVR_EPG_RECORD) + .title(getResources().getString(R.string.dvr_epg_record)) + .build()); + actions.add(new GuidedAction.Builder(activity) + .id(DVR_EPG_NOT_RECORD) + .title(getResources().getString(R.string.dvr_epg_do_not_record)) + .build()); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + Program program = getEntry().program; + if (action.getId() == DVR_EPG_RECORD) { + getDvrManager().addSchedule(program, mConflicts); + } + dismissDialog(); + } +} diff --git a/src/com/android/tv/dvr/ui/DvrRecordDeleteFragment.java b/src/com/android/tv/dvr/ui/DvrRecordDeleteFragment.java new file mode 100644 index 00000000..d4d5cc41 --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrRecordDeleteFragment.java @@ -0,0 +1,48 @@ +package com.android.tv.dvr.ui; + +import android.app.Activity; +import android.os.Bundle; + +import android.support.v17.leanback.widget.GuidanceStylist.Guidance; +import android.support.v17.leanback.widget.GuidedAction; + +import com.android.tv.R; +import com.android.tv.guide.ProgramManager.TableEntry; + +import java.util.List; + +public class DvrRecordDeleteFragment extends DvrGuidedStepFragment { + private static final int ACTION_DELETE_YES = 1; + private static final int ACTION_DELETE_NO = 2; + + public DvrRecordDeleteFragment(TableEntry entry) { + super(entry); + } + + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getResources().getString(R.string.epg_dvr_dialog_message_delete_schedule); + return new Guidance(title, null, null, null); + } + + @Override + public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { + Activity activity = getActivity(); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_DELETE_YES) + .title(getResources().getString(android.R.string.yes)) + .build()); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_DELETE_NO) + .title(getResources().getString(android.R.string.no)) + .build()); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + if (action.getId() == ACTION_DELETE_YES) { + getDvrManager().removeScheduledRecording(getEntry().scheduledRecording); + } + dismissDialog(); + } +} diff --git a/src/com/android/tv/dvr/ui/DvrRecordScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrRecordScheduleFragment.java new file mode 100644 index 00000000..77e78ccc --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrRecordScheduleFragment.java @@ -0,0 +1,70 @@ +package com.android.tv.dvr.ui; + +import android.app.Activity; +import android.app.FragmentManager; +import android.os.Bundle; + +import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidanceStylist.Guidance; +import android.support.v17.leanback.widget.GuidedAction; + +import com.android.tv.data.Program; +import com.android.tv.dialog.SafeDismissDialogFragment; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.guide.ProgramManager.TableEntry; +import com.android.tv.MainActivity; +import com.android.tv.R; + +import java.util.List; + +public class DvrRecordScheduleFragment extends DvrGuidedStepFragment { + private static final int ACTION_RECORD_YES = 1; + private static final int ACTION_RECORD_NO = 2; + + public DvrRecordScheduleFragment(TableEntry entry) { + super(entry); + } + + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getResources().getString(R.string.epg_dvr_dialog_message_schedule_recording); + return new Guidance(title, null, null, null); + } + + @Override + public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { + Activity activity = getActivity(); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_RECORD_YES) + .title(getResources().getString(android.R.string.yes)) + .build()); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_RECORD_NO) + .title(getResources().getString(android.R.string.no)) + .build()); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + TableEntry entry = getEntry(); + Program program = entry.program; + final List<ScheduledRecording> conflicts = + getDvrManager().getScheduledRecordingsThatConflict(program); + if (action.getId() == ACTION_RECORD_YES) { + if (conflicts.isEmpty()) { + getDvrManager().addSchedule(program, conflicts); + dismissDialog(); + } else { + DvrRecordConflictFragment dvrConflict = new DvrRecordConflictFragment(entry); + SafeDismissDialogFragment currentDialog = + ((MainActivity) getActivity()).getOverlayManager().getCurrentDialog(); + if (currentDialog instanceof DvrDialogFragment) { + FragmentManager fm = currentDialog.getChildFragmentManager(); + GuidedStepFragment.add(fm, dvrConflict, R.id.halfsized_dialog_host); + } + } + } else if (action.getId() == ACTION_RECORD_NO) { + dismissDialog(); + } + } +} diff --git a/src/com/android/tv/dvr/ui/EmptyHolder.java b/src/com/android/tv/dvr/ui/EmptyHolder.java new file mode 100644 index 00000000..45cd3a36 --- /dev/null +++ b/src/com/android/tv/dvr/ui/EmptyHolder.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +/** + * Special object meaning a row is empty; + */ +final class EmptyHolder { + static final EmptyHolder EMPTY_HOLDER = new EmptyHolder(); + + private EmptyHolder() { + } +} diff --git a/src/com/android/tv/dvr/ui/EmptyItemPresenter.java b/src/com/android/tv/dvr/ui/EmptyItemPresenter.java new file mode 100644 index 00000000..c0305128 --- /dev/null +++ b/src/com/android/tv/dvr/ui/EmptyItemPresenter.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.content.res.Resources; +import android.graphics.Color; +import android.support.v17.leanback.widget.Presenter; +import android.view.Gravity; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.util.Utils; + +/** + * Shows the item "NONE". Used for rows with now items. + */ +public class EmptyItemPresenter extends Presenter { + + private final DvrBrowseFragment mMainFragment; + + public EmptyItemPresenter(DvrBrowseFragment mainFragment) { + mMainFragment = mainFragment; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + TextView view = new TextView(parent.getContext()); + Resources resources = view.getResources(); + view.setLayoutParams(new ViewGroup.LayoutParams( + resources.getDimensionPixelSize(R.dimen.dvr_card_layout_width), + resources.getDimensionPixelSize(R.dimen.dvr_card_layout_width))); + view.setFocusable(true); + view.setFocusableInTouchMode(true); + view.setBackgroundColor( + Utils.getColor(mMainFragment.getResources(), R.color.setup_background)); + view.setTextColor(Color.WHITE); + view.setGravity(Gravity.CENTER); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder viewHolder, Object recording) { + ((TextView) viewHolder.view).setText( + viewHolder.view.getContext().getString(R.string.dvr_msg_no_recording_on_the_row)); + } + + @Override + public void onUnbindViewHolder(ViewHolder viewHolder) { } +} diff --git a/src/com/android/tv/dvr/ui/GridItemPresenter.java b/src/com/android/tv/dvr/ui/GridItemPresenter.java deleted file mode 100644 index 099816d4..00000000 --- a/src/com/android/tv/dvr/ui/GridItemPresenter.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.graphics.Color; -import android.support.v17.leanback.widget.Presenter; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import android.widget.Toast; - -import com.android.tv.MainActivity; -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; -import com.android.tv.data.Program; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.Recording; -import com.android.tv.util.Utils; - -import java.util.List; - -public class GridItemPresenter extends Presenter { - private static final int GRID_ITEM_WIDTH = 200; - private static final int GRID_ITEM_HEIGHT = 200; - - private final DvrBrowseFragment mainFragment; - - public GridItemPresenter(DvrBrowseFragment mainFragment) { - this.mainFragment = mainFragment; - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - TextView view = new TextView(parent.getContext()); - view.setLayoutParams(new ViewGroup.LayoutParams(GRID_ITEM_WIDTH, GRID_ITEM_HEIGHT)); - view.setFocusable(true); - view.setFocusableInTouchMode(true); - view.setBackgroundColor( - Utils.getColor(mainFragment.getResources(), R.color.setup_background)); - view.setTextColor(Color.WHITE); - view.setGravity(Gravity.CENTER); - return new ViewHolder(view); - } - - @Override - public void onBindViewHolder(ViewHolder viewHolder, Object recording) { - if (recording == null) { - ((TextView) viewHolder.view).setText(viewHolder.view.getContext() - .getString(R.string.dvr_msg_no_recording_on_the_row)); - } else { - final Recording r = (Recording) recording; - StringBuilder sb = new StringBuilder(); - List<Program> programs = r.getPrograms(); - if (programs != null && programs.size() > 0) { - sb.append(programs.get(0).getTitle()); - } else { - sb.append(viewHolder.view.getContext() - .getString(R.string.dvr_msg_program_title_unknown)); - } - sb.append(" "); - Channel channel = r.getChannel(); - if (channel != null) { - sb.append(channel.getDisplayName()); - } else { - sb.append(viewHolder.view.getContext().getString(R.string.dvr_msg_channel_unknown)); - } - sb.append(" ").append(Utils.toIsoDateTimeString(r.getStartTimeMs())); - ((TextView) viewHolder.view).setText(sb.toString()); - final Context context = viewHolder.view.getContext(); - viewHolder.view.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - switch (r.getState()) { - case Recording.STATE_RECORDING_NOT_STARTED: { - new AlertDialog.Builder(context) - .setNegativeButton(R.string.dvr_detail_cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Toast.makeText(context, "Not implemented yet", - Toast.LENGTH_SHORT).show(); - } - }) - .show(); - break; - } - case Recording.STATE_RECORDING_IN_PROGRESS: { - new AlertDialog.Builder(context) - .setNegativeButton(R.string.dvr_detail_stop_delete, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Toast.makeText(context, "Not implemented yet", - Toast.LENGTH_SHORT).show(); - } - }) - .setPositiveButton(R.string.dvr_detail_stop_keep, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Toast.makeText(context, "Not implemented yet", - Toast.LENGTH_SHORT).show(); - } - }) - .show(); - break; - } - case Recording.STATE_RECORDING_FINISHED: { - new AlertDialog.Builder(context) - .setNegativeButton(R.string.dvr_detail_delete, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - DvrManager dvrManager = TvApplication - .getSingletons(mainFragment.getContext()) - .getDvrManager(); - // TODO(DVR) handle success/failure. - dvrManager.removeRecording(r); - } - }) - .setPositiveButton(R.string.dvr_detail_play, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent(context, MainActivity.class); - intent.putExtra(Utils.EXTRA_KEY_RECORDING_URI, - r.getUri()); - context.startActivity(intent); - ((Activity) context).finish(); - } - }) - .show(); - break; - } - } - } - }); - } - } - - @Override - public void onUnbindViewHolder(ViewHolder viewHolder) { - } -}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java new file mode 100644 index 00000000..dc89a8e0 --- /dev/null +++ b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java @@ -0,0 +1,30 @@ +package com.android.tv.dvr.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.dialog.SafeDismissDialogFragment; +import com.android.tv.R; + +public class HalfSizedDialogFragment extends SafeDismissDialogFragment { + public static final String DIALOG_TAG = HalfSizedDialogFragment.class.getSimpleName(); + public static final String TRACKER_LABEL = "Half sized dialog"; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.halfsized_dialog, null); + } + + @Override + public int getTheme() { + return R.style.Theme_TV_dialog_HalfSizedDialog; + } + + @Override + public String getTrackerLabel() { + return TRACKER_LABEL; + } +} diff --git a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java new file mode 100644 index 00000000..0b656bdc --- /dev/null +++ b/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.media.tv.TvContract; +import android.support.v17.leanback.widget.Presenter; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; + +import java.util.List; + +import com.android.tv.MainActivity; +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.common.recording.RecordedProgram; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.ui.DialogUtils; +import com.android.tv.util.Utils; + +/** + * Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}. + */ +public class RecordedProgramPresenter extends Presenter { + private final ChannelDataManager mChannelDataManager; + + public RecordedProgramPresenter(Context context) { + mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager(); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + Context context = parent.getContext(); + RecordingCardView view = new RecordingCardView(context); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder viewHolder, Object o) { + final RecordedProgram recording = (RecordedProgram) o; + final RecordingCardView cardView = (RecordingCardView) viewHolder.view; + final Context context = viewHolder.view.getContext(); + final Resources resources = context.getResources(); + + Channel channel = mChannelDataManager.getChannel(recording.getChannelId()); + + if (!TextUtils.isEmpty(recording.getTitle())) { + cardView.setTitle(recording.getTitle()); + } else { + cardView.setTitle(resources.getString(R.string.dvr_msg_program_title_unknown)); + } + if (recording.getPosterArt() != null) { + cardView.setImageUri(recording.getPosterArt()); + } else if (recording.getThumbnail() != null) { + cardView.setImageUri(recording.getThumbnail()); + } else { + if (channel != null) { + cardView.setImageUri(TvContract.buildChannelLogoUri(channel.getId()).toString()); + } + } + cardView.setContent(Utils.getDurationString(context, recording.getStartTimeUtcMillis(), + recording.getEndTimeUtcMillis(), true)); + //TODO: replace with a detail card + viewHolder.view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + DialogUtils.showListDialog(v.getContext(), + new int[] { R.string.dvr_detail_play, R.string.dvr_detail_delete }, + new Runnable[] { + new Runnable() { + @Override + public void run() { + Intent intent = new Intent(context, MainActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDING_URI, + recording.getUri()); + context.startActivity(intent); + ((Activity) context).finish(); + } + }, + new Runnable() { + @Override + public void run() { + DvrManager dvrManager = TvApplication + .getSingletons(context).getDvrManager(); + dvrManager.removeRecordedProgram(recording); + } + }, + }); + } + }); + + } + + @Override + public void onUnbindViewHolder(ViewHolder viewHolder) { + final RecordingCardView cardView = (RecordingCardView) viewHolder.view; + cardView.reset(); + } +} diff --git a/src/com/android/tv/dvr/ui/RecordedProgramsAdapter.java b/src/com/android/tv/dvr/ui/RecordedProgramsAdapter.java new file mode 100644 index 00000000..eeb26041 --- /dev/null +++ b/src/com/android/tv/dvr/ui/RecordedProgramsAdapter.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.support.v17.leanback.widget.PresenterSelector; + +import com.android.tv.common.recording.RecordedProgram; +import com.android.tv.dvr.DvrDataManager; + +/** + * Adapter for {@link RecordedProgram}. + */ +final class RecordedProgramsAdapter extends SortedArrayAdapter<RecordedProgram> + implements DvrDataManager.RecordedProgramListener { + private final DvrDataManager mDataManager; + + RecordedProgramsAdapter(DvrDataManager dataManager, PresenterSelector presenterSelector) { + super(presenterSelector, RecordedProgram.START_TIME_THEN_ID_COMPARATOR); + mDataManager = dataManager; + } + + public void start() { + clear(); + addAll(mDataManager.getRecordedPrograms()); + mDataManager.addRecordedProgramListener(this); + } + + public void stop() { + mDataManager.removeRecordedProgramListener(this); + } + + @Override + long getId(RecordedProgram item) { + return item.getId(); + } + + @Override // DvrDataManager.RecordedProgramListener + public void onRecordedProgramAdded(RecordedProgram recordedProgram) { + add(recordedProgram); + } + + @Override // DvrDataManager.RecordedProgramListener + public void onRecordedProgramChanged(RecordedProgram recordedProgram) { + change(recordedProgram); + } + + @Override // DvrDataManager.RecordedProgramListener + public void onRecordedProgramRemoved(RecordedProgram recordedProgram) { + remove(recordedProgram); + } +} diff --git a/src/com/android/tv/dvr/ui/RecordingCardView.java b/src/com/android/tv/dvr/ui/RecordingCardView.java new file mode 100644 index 00000000..def11248 --- /dev/null +++ b/src/com/android/tv/dvr/ui/RecordingCardView.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.support.annotation.Nullable; +import android.support.v17.leanback.widget.BaseCardView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.tv.R; +import com.android.tv.util.ImageLoader; + +/** + * A CardView for displaying info about a {@link com.android.tv.dvr.ScheduledRecording} or + * {@link com.android.tv.common.recording.RecordedProgram} + */ +class RecordingCardView extends BaseCardView { + private final ImageView mImageView; + private final int mImageWidth; + private final int mImageHeight; + private String mImageUri; + private final TextView mTitleView; + private final TextView mContentView; + private final Drawable mDefaultImage; + + RecordingCardView(Context context) { + super(context); + //TODO(dvr): move these to the layout XML. + setCardType(BaseCardView.CARD_TYPE_INFO_UNDER_WITH_EXTRA); + setFocusable(true); + setFocusableInTouchMode(true); + mDefaultImage = getResources().getDrawable(R.drawable.default_now_card, null); + + LayoutInflater inflater = LayoutInflater.from(getContext()); + inflater.inflate(R.layout.dvr_recording_card_view, this); + + mImageView = (ImageView) findViewById(R.id.image); + mImageWidth = getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_width); + mImageHeight = getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_width); + mTitleView = (TextView) findViewById(R.id.title); + mContentView = (TextView) findViewById(R.id.content); + } + + void setTitle(CharSequence title) { + mTitleView.setText(title); + } + + void setContent(CharSequence content) { + mContentView.setText(content); + } + + void setImageUri(String uri) { + mImageUri = uri; + if (TextUtils.isEmpty(uri)) { + mImageView.setImageDrawable(mDefaultImage); + } else { + ImageLoader.loadBitmap(getContext(), uri, mImageWidth, mImageHeight, + new RecordingCardImageLoaderCallback(this, uri)); + } + } + + public void setImageUri(Uri uri) { + if (uri != null) { + setImageUri(uri.toString()); + } else { + setImageUri(""); + } + } + + private static class RecordingCardImageLoaderCallback + extends ImageLoader.ImageLoaderCallback<RecordingCardView> { + private final String mUri; + + RecordingCardImageLoaderCallback(RecordingCardView referent, String uri) { + super(referent); + mUri = uri; + } + + @Override + public void onBitmapLoaded(RecordingCardView view, @Nullable Bitmap bitmap) { + if (bitmap == null || !mUri.equals(view.mImageUri)) { + view.mImageView.setImageDrawable(view.mDefaultImage); + } else { + view.mImageView.setImageDrawable(new BitmapDrawable(view.getResources(), bitmap)); + } + } + } + + public void reset() { + mTitleView.setText(""); + mContentView.setText(""); + mImageView.setImageDrawable(mDefaultImage); + } +} diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java new file mode 100644 index 00000000..533a4882 --- /dev/null +++ b/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.media.tv.TvContract; +import android.support.annotation.Nullable; +import android.support.v17.leanback.widget.Presenter; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import com.android.tv.ApplicationSingletons; +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.Program; +import com.android.tv.data.ProgramDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.util.Utils; + +/** + * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. + */ +public class ScheduledRecordingPresenter extends Presenter { + private final ChannelDataManager mChannelDataManager; + + private static final class ScheduledRecordingViewHolder extends ViewHolder { + private ProgramDataManager.QueryProgramTask mQueryProgramTask; + + ScheduledRecordingViewHolder(RecordingCardView view) { + super(view); + } + } + + public ScheduledRecordingPresenter(Context context) { + ApplicationSingletons singletons = TvApplication.getSingletons(context); + mChannelDataManager = singletons.getChannelDataManager(); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + Context context = parent.getContext(); + RecordingCardView view = new RecordingCardView(context); + return new ScheduledRecordingViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder baseHolder, Object o) { + ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; + final ScheduledRecording recording = (ScheduledRecording) o; + final RecordingCardView cardView = (RecordingCardView) viewHolder.view; + final Context context = viewHolder.view.getContext(); + + long programId = recording.getProgramId(); + if (programId == ScheduledRecording.ID_NOT_SET) { + setTitleAndImage(cardView, recording, null); + } else { + viewHolder.mQueryProgramTask = new ProgramDataManager.QueryProgramTask( + context.getContentResolver(), programId) { + @Override + protected void onPostExecute(Program program) { + super.onPostExecute(program); + setTitleAndImage(cardView, recording, program); + } + }; + viewHolder.mQueryProgramTask.executeOnDbThread(); + + } + cardView.setContent(Utils.getDurationString(context, recording.getStartTimeMs(), + recording.getEndTimeMs(), true)); + //TODO: replace with a detail card + View.OnClickListener clickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + switch (recording.getState()) { + case ScheduledRecording.STATE_RECORDING_NOT_STARTED: { + showScheduledRecordingDialog(v.getContext(), recording); + break; + } + case ScheduledRecording.STATE_RECORDING_IN_PROGRESS: { + showCurrentlyRecordingDialog(v.getContext(), recording); + break; + } + } + } + }; + baseHolder.view.setOnClickListener(clickListener); + } + + private void setTitleAndImage(RecordingCardView cardView, ScheduledRecording recording, + @Nullable Program program) { + if (program != null) { + cardView.setTitle(program.getTitle()); + cardView.setImageUri(program.getPosterArtUri()); + } else { + cardView.setTitle( + cardView.getResources().getString(R.string.dvr_msg_program_title_unknown)); + Channel channel = mChannelDataManager.getChannel(recording.getChannelId()); + if (channel != null) { + cardView.setImageUri(TvContract.buildChannelLogoUri(channel.getId()).toString()); + } + } + } + + @Override + public void onUnbindViewHolder(ViewHolder baseHolder) { + ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; + final RecordingCardView cardView = (RecordingCardView) viewHolder.view; + if (viewHolder.mQueryProgramTask != null) { + viewHolder.mQueryProgramTask.cancel(true); + viewHolder.mQueryProgramTask = null; + } + cardView.reset(); + } + + private void showScheduledRecordingDialog(final Context context, + final ScheduledRecording recording) { + DialogInterface.OnClickListener removeScheduleListener + = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // TODO(DVR) handle success/failure. + DvrManager dvrManager = TvApplication.getSingletons(context) + .getDvrManager(); + dvrManager.removeScheduledRecording((ScheduledRecording) recording); + } + }; + new AlertDialog.Builder(context) + .setMessage(R.string.epg_dvr_dialog_message_remove_recording_schedule) + .setNegativeButton(android.R.string.no, null) + .setPositiveButton(android.R.string.yes, removeScheduleListener) + .show(); + } + + private void showCurrentlyRecordingDialog(final Context context, + final ScheduledRecording recording) { + DialogInterface.OnClickListener stopRecordingListener + = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + DvrManager dvrManager = TvApplication.getSingletons(context) + .getDvrManager(); + dvrManager.stopRecording((ScheduledRecording) recording); + } + }; + new AlertDialog.Builder(context) + .setMessage(R.string.epg_dvr_dialog_message_stop_recording) + .setNegativeButton(android.R.string.no, null) + .setPositiveButton(android.R.string.yes, stopRecordingListener) + .show(); + } +} diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingsAdapter.java b/src/com/android/tv/dvr/ui/ScheduledRecordingsAdapter.java new file mode 100644 index 00000000..65955276 --- /dev/null +++ b/src/com/android/tv/dvr/ui/ScheduledRecordingsAdapter.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.support.v17.leanback.widget.PresenterSelector; + +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.ScheduledRecording; + +/** + * Adapter for {@link ScheduledRecording} filtered by + * {@link com.android.tv.dvr.ScheduledRecording.RecordingState}. + */ +final class ScheduledRecordingsAdapter extends SortedArrayAdapter<ScheduledRecording> + implements DvrDataManager.ScheduledRecordingListener { + private final int mState; + private final DvrDataManager mDataManager; + + ScheduledRecordingsAdapter(DvrDataManager dataManager, int state, + PresenterSelector presenterSelector) { + super(presenterSelector, ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR); + mDataManager = dataManager; + mState = state; + } + + public void start() { + clear(); + switch (mState) { + case ScheduledRecording.STATE_RECORDING_NOT_STARTED: + addAll(mDataManager.getNonStartedScheduledRecordings()); + break; + case ScheduledRecording.STATE_RECORDING_IN_PROGRESS: + addAll(mDataManager.getStartedRecordings()); + break; + default: + throw new IllegalStateException("Unknown recording state " + mState); + + } + mDataManager.addScheduledRecordingListener(this); + } + + public void stop() { + mDataManager.removeScheduledRecordingListener(this); + } + + @Override + long getId(ScheduledRecording item) { + return item.getId(); + } + + @Override //DvrDataManager.ScheduledRecordingListener + public void onScheduledRecordingAdded(ScheduledRecording scheduledRecording) { + if (scheduledRecording.getState() == mState) { + add(scheduledRecording); + } + } + + @Override //DvrDataManager.ScheduledRecordingListener + public void onScheduledRecordingRemoved(ScheduledRecording scheduledRecording) { + remove(scheduledRecording); + } + + @Override //DvrDataManager.ScheduledRecordingListener + public void onScheduledRecordingStatusChanged(ScheduledRecording scheduledRecording) { + if (scheduledRecording.getState() == mState) { + change(scheduledRecording); + } else { + remove(scheduledRecording); + } + } +} diff --git a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java new file mode 100644 index 00000000..8a8bcdeb --- /dev/null +++ b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.support.v17.leanback.widget.ObjectAdapter; +import android.support.v17.leanback.widget.PresenterSelector; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Keeps a set of {@code T} items sorted, but leaving a {@link EmptyHolder} + * if there is no items. + * + * <p>{@code T} must have stable IDs. + */ +abstract class SortedArrayAdapter<T> extends ObjectAdapter { + private final List<T> mItems = new ArrayList<>(); + private final Comparator<T> mComparator; + + SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator) { + super(presenterSelector); + mComparator = comparator; + setHasStableIds(true); + } + + @Override + public final int size() { + return mItems.isEmpty() ? 1 : mItems.size(); + } + + @Override + public final Object get(int position) { + return isEmpty() ? EmptyHolder.EMPTY_HOLDER : getItem(position); + } + + @Override + public final long getId(int position) { + if (isEmpty()) { + return NO_ID; + } + T item = mItems.get(position); + return item == null ? NO_ID : getId(item); + } + + /** + * Returns the id of the the given {@code item}. + * + * The id must be stable. + */ + abstract long getId(T item); + + /** + * Returns the item at the given {@code position}. + * + * @throws IndexOutOfBoundsException if the position is out of range + * (<tt>position < 0 || position >= size()</tt>) + */ + final T getItem(int position) { + return mItems.get(position); + } + + /** + * Returns {@code true} if the list of items is empty. + * + * <p><b>NOTE</b> when the item list is empty the adapter has a size of 1 and + * {@link EmptyHolder#EMPTY_HOLDER} at position 0; + */ + final boolean isEmpty() { + return mItems.isEmpty(); + } + + /** + * Removes all elements from the list. + * + * <p><b>NOTE</b> when the item list is empty the adapter has a size of 1 and + * {@link EmptyHolder#EMPTY_HOLDER} at position 0; + */ + final void clear() { + mItems.clear(); + notifyChanged(); + } + + /** + * Adds the objects in the given collection to the adapter keeping the elements sorted. + * If the index is >= {@link #size} an exception will be thrown. + * + * @param items A {@link Collection} of items to insert. + */ + final void addAll(Collection<T> items) { + mItems.addAll(items); + Collections.sort(mItems, mComparator); + notifyChanged(); + } + + /** + * Adds an item in sorted order to the adapter. + * + * @param item The item to add in sorted order to the adapter. + */ + final void add(T item) { + int i = findWhereToInsert(item); + mItems.add(i, item); + if (mItems.size() == 1) { + notifyItemRangeChanged(0, 1); + } else { + notifyItemRangeInserted(i, 1); + } + } + + /** + * Remove an item from the list + * + * @param item The item to remove from the adapter. + */ + final void remove(T item) { + int index = indexOf(item); + if (index != -1) { + mItems.remove(index); + if (mItems.isEmpty()) { + notifyItemRangeChanged(0, 1); + } else { + notifyItemRangeRemoved(index, 1); + } + } + } + + /** + * Change an item in the list. + * @param item The item to change. + */ + final void change(T item) { + int oldIndex = indexOf(item); + if (oldIndex != -1) { + T old = mItems.get(oldIndex); + if (mComparator.compare(old, item) == 0) { + mItems.set(oldIndex, item); + notifyItemRangeChanged(oldIndex, 1); + return; + } + mItems.remove(oldIndex); + } + int newIndex = findWhereToInsert(item); + mItems.add(newIndex, item); + + if (oldIndex != -1) { + notifyItemRangeRemoved(oldIndex, 1); + } + if (newIndex != -1) { + notifyItemRangeInserted(newIndex, 1); + } + } + + private int indexOf(T item) { + long id = getId(item); + for (int i = 0; i < mItems.size(); i++) { + T r = mItems.get(i); + if (getId(r) == id) { + return i; + } + } + return -1; + } + + private int findWhereToInsert(T item) { + int i; + int size = mItems.size(); + for (i = 0; i < size; i++) { + T r = mItems.get(i); + if (mComparator.compare(r, item) > 0) { + return i; + } + } + return size; + } +} diff --git a/src/com/android/tv/guide/ProgramGrid.java b/src/com/android/tv/guide/ProgramGrid.java index 1339ddf8..77de5827 100644 --- a/src/com/android/tv/guide/ProgramGrid.java +++ b/src/com/android/tv/guide/ProgramGrid.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; import android.support.v17.leanback.widget.VerticalGridView; +import android.support.v7.widget.RecyclerView.LayoutManager; import android.util.AttributeSet; import android.util.Log; import android.view.View; @@ -66,6 +67,16 @@ public class ProgramGrid extends VerticalGridView { } }; + private final ViewTreeObserver.OnPreDrawListener mPreDrawListener = + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + getViewTreeObserver().removeOnPreDrawListener(this); + updateInputLogo(); + return true; + } + }; + private ProgramManager mProgramManager; private View mNextFocusByUpDown; @@ -83,7 +94,7 @@ public class ProgramGrid extends VerticalGridView { private boolean mKeepCurrentProgram; private ChildFocusListener mChildFocusListener; - private OnRepeatedKeyInterceptListener mOnRepeatedKeyInterceptListener; + private final OnRepeatedKeyInterceptListener mOnRepeatedKeyInterceptListener; interface ChildFocusListener { /** @@ -332,6 +343,10 @@ public class ProgramGrid extends VerticalGridView { return contains((View) v.getParent()); } + public void onItemSelectionReset() { + getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); + } + @Override public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { if (mLastFocusedView != null && mLastFocusedView.isShown()) { @@ -359,6 +374,49 @@ public class ProgramGrid extends VerticalGridView { int maxY = (mSelectionRow + 1) * mRowHeight + mDetailHeight; if (y > maxY) scrollBy(0, y - maxY); } + updateInputLogo(); + } + + @Override + public void onViewRemoved(View view) { + // It is required to ensure input logo showing when the scroll is moved to most bottom. + updateInputLogo(); + } + + private int getFirstVisibleChildIndex() { + final LayoutManager mLayoutManager = getLayoutManager(); + int top = mLayoutManager.getPaddingTop(); + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View childView = getChildAt(i); + int childTop = mLayoutManager.getDecoratedTop(childView); + int childBottom = mLayoutManager.getDecoratedBottom(childView); + if ((childTop + childBottom) / 2 > top) { + return i; + } + } + return -1; + } + + public void updateInputLogo() { + int childCount = getChildCount(); + if (childCount == 0) { + return; + } + int firstVisibleChildIndex = getFirstVisibleChildIndex(); + if (firstVisibleChildIndex == -1) { + return; + } + View childView = getChildAt(firstVisibleChildIndex); + int childAdapterPosition = getChildAdapterPosition(childView); + ((ProgramTableAdapter.ProgramRowHolder) getChildViewHolder(childView)) + .updateInputLogo(childAdapterPosition, true); + for (int i = firstVisibleChildIndex + 1; i < childCount; i++) { + childView = getChildAt(i); + ((ProgramTableAdapter.ProgramRowHolder) getChildViewHolder(childView)) + .updateInputLogo(childAdapterPosition, false); + childAdapterPosition = getChildAdapterPosition(childView); + } } private static void findFocusables(View v, ArrayList<View> outFocusable) { diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java index 77a1146b..bfcb8b0d 100644 --- a/src/com/android/tv/guide/ProgramGuide.java +++ b/src/com/android/tv/guide/ProgramGuide.java @@ -31,6 +31,7 @@ import android.os.Message; import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v17.leanback.widget.OnChildSelectedListener; import android.support.v17.leanback.widget.SearchOrbView; import android.support.v17.leanback.widget.VerticalGridView; @@ -52,6 +53,7 @@ import com.android.tv.common.WeakHandler; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.GenreItems; import com.android.tv.data.ProgramDataManager; +import com.android.tv.dvr.DvrDataManager; import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -160,12 +162,11 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { public ProgramGuide(MainActivity activity, ChannelTuner channelTuner, TvInputManagerHelper tvInputManagerHelper, ChannelDataManager channelDataManager, - ProgramDataManager programDataManager, Tracker tracker, Runnable preShowRunnable, - Runnable postHideRunnable) { + ProgramDataManager programDataManager, @Nullable DvrDataManager dvrDataManager, + Tracker tracker, Runnable preShowRunnable, Runnable postHideRunnable) { mActivity = activity; - mProgramManager = new ProgramManager(tvInputManagerHelper, - channelDataManager, - programDataManager); + mProgramManager = new ProgramManager(tvInputManagerHelper, channelDataManager, + programDataManager, dvrDataManager); mChannelTuner = channelTuner; mTracker = tracker; mPreShowRunnable = preShowRunnable; @@ -245,7 +246,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mTimelineRow.setAdapter(mTimeListAdapter); ProgramTableAdapter programTableAdapter = new ProgramTableAdapter(mActivity, - tvInputManagerHelper, mProgramManager, this); + mProgramManager, this); programTableAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { @@ -590,7 +591,10 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { return mTimelineRow.getScrollOffset(); } - private void cancelHide() { + /** + * Cancel hiding the program guide. + */ + public void cancelHide() { mHandler.removeCallbacks(mHideRunnable); } @@ -720,6 +724,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { Math.max(mProgramManager.getChannelIndex(mChannelTuner.getCurrentChannel()), 0)); mGrid.resetFocusState(); + mGrid.onItemSelectionReset(); mIsDuringResetRowSelection = false; } diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java index 09a93037..172ee070 100644 --- a/src/com/android/tv/guide/ProgramItemView.java +++ b/src/com/android/tv/guide/ProgramItemView.java @@ -16,9 +16,7 @@ package com.android.tv.guide; -import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.drawable.Drawable; @@ -26,6 +24,7 @@ import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.StateListDrawable; import android.os.Handler; import android.os.SystemClock; +import android.support.v4.os.BuildCompat; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; @@ -43,15 +42,14 @@ import com.android.tv.TvApplication; import com.android.tv.analytics.Tracker; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; -import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.Recording; +import com.android.tv.dvr.ui.DvrDialogFragment; +import com.android.tv.dvr.ui.DvrRecordDeleteFragment; +import com.android.tv.dvr.ui.DvrRecordScheduleFragment; import com.android.tv.guide.ProgramManager.TableEntry; import com.android.tv.util.Utils; import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.TimeUnit; public class ProgramItemView extends TextView { @@ -60,9 +58,6 @@ public class ProgramItemView extends TextView { private static final long FOCUS_UPDATE_FREQUENCY = TimeUnit.SECONDS.toMillis(1); private static final int MAX_PROGRESS = 10000; // From android.widget.ProgressBar.MAX_VALUE - private static final int ACTION_RECORD_PROGRAM = 100; - private static final int ACTION_RECORD_SEASON = 101; - // State indicating the focused program is the current program private static final int[] STATE_CURRENT_PROGRAM = { R.attr.state_current_program }; @@ -89,6 +84,10 @@ public class ProgramItemView extends TextView { @Override public void onClick(final View view) { TableEntry entry = ((ProgramItemView) view).mTableEntry; + if (entry == null) { + //do nothing + return; + } ApplicationSingletons singletons = TvApplication.getSingletons(view.getContext()); Tracker tracker = singletons.getTracker(); tracker.sendEpgItemClicked(); @@ -105,78 +104,38 @@ public class ProgramItemView extends TextView { }, entry.getWidth() > ((ProgramItemView) view).mMaxWidthForRipple ? 0 : view.getResources() .getInteger(R.integer.program_guide_ripple_anim_duration)); - } else if (CommonFeatures.DVR.isEnabled(view.getContext())) { + } else if (CommonFeatures.DVR.isEnabled(view.getContext()) && BuildCompat + .isAtLeastN()) { final MainActivity tvActivity = (MainActivity) view.getContext(); final DvrManager dvrManager = singletons.getDvrManager(); final Channel channel = tvActivity.getChannelDataManager() .getChannel(entry.channelId); - if (dvrManager.canRecord(channel.getInputId())) { - showDvrDialog(view, entry, dvrManager); + if (dvrManager.canRecord(channel.getInputId()) && entry.program != null) { + if (entry.scheduledRecording == null) { + showDvrDialog(view, entry); + } else { + showRecordDeleteDialog(view, entry); + } } } } - private void showDvrDialog(final View view, TableEntry entry, final DvrManager dvrManager) { - List<CharSequence> items = new ArrayList<>(); - final List<Integer> actions = new ArrayList<>(); - // TODO: the items can be changed by the state of the program. For example, - // if the program is already added in scheduler, we need to show an item to - // delete the recording schedule. - items.add(view.getResources().getString(R.string.epg_dvr_record_program)); - actions.add(ACTION_RECORD_PROGRAM); - items.add(view.getResources().getString(R.string.epg_dvr_record_season)); - actions.add(ACTION_RECORD_SEASON); - - final Program program = entry.program; - final List<Recording> conflicts = dvrManager - .getScheduledRecordingsThatConflict(program); - // TODO: it is a tentative UI. Don't publish the UI. - DialogInterface.OnClickListener onClickListener - = new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, int which) { - if (actions.get(which) == ACTION_RECORD_PROGRAM) { - if (conflicts.isEmpty()) { - dvrManager.addSchedule(program, conflicts); - } else { - showConflictDialog(view, dvrManager, program, conflicts); - } - } else if (actions.get(which) == ACTION_RECORD_SEASON) { - dvrManager.addSeasonSchedule(program); - } - dialog.dismiss(); - } - }; - new AlertDialog.Builder(view.getContext()) - .setItems(items.toArray(new CharSequence[items.size()]), onClickListener) - .create() - .show(); + private void showDvrDialog(final View view, TableEntry entry) { + Utils.showToastMessageForDeveloperFeature(view.getContext()); + DvrRecordScheduleFragment dvrRecordScheduleFragment = + new DvrRecordScheduleFragment(entry); + DvrDialogFragment dvrDialogFragment = new DvrDialogFragment(dvrRecordScheduleFragment); + ((MainActivity) view.getContext()).getOverlayManager().showDialogFragment( + DvrDialogFragment.DIALOG_TAG, dvrDialogFragment, true, true); } - }; - private static void showConflictDialog(final View view, final DvrManager dvrManager, - final Program program, final List<Recording> conflicts) { - DialogInterface.OnClickListener conflictClickListener - = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == AlertDialog.BUTTON_POSITIVE) { - dvrManager.addSchedule(program, conflicts); - dialog.dismiss(); - } - } - }; - StringBuilder sb = new StringBuilder(); - for (Recording r : conflicts) { - sb.append(r.toString()).append('\n'); + private void showRecordDeleteDialog(final View view, final TableEntry entry) { + DvrRecordDeleteFragment recordDeleteDialogFragment = new DvrRecordDeleteFragment(entry); + DvrDialogFragment dvrDialogFragment = new DvrDialogFragment(recordDeleteDialogFragment); + ((MainActivity) view.getContext()).getOverlayManager().showDialogFragment( + DvrDialogFragment.DIALOG_TAG, dvrDialogFragment, true, true); } - new AlertDialog.Builder(view.getContext()).setTitle(R.string.dvr_epg_conflict_dialog_title) - .setMessage(sb.toString()) - .setPositiveButton(R.string.dvr_epg_record, conflictClickListener) - .setNegativeButton(R.string.dvr_epg_do_not_record, conflictClickListener) - .create() - .show(); - } + }; private static final View.OnFocusChangeListener ON_FOCUS_CHANGED = new View.OnFocusChangeListener() { @@ -198,6 +157,10 @@ public class ProgramItemView extends TextView { public void run() { refreshDrawableState(); TableEntry entry = mTableEntry; + if (entry == null) { + //do nothing + return; + } if (entry.isCurrentProgram()) { Drawable background = getBackground(); int progress = getProgress(entry.entryStartUtcMillis, entry.entryEndUtcMillis); @@ -220,6 +183,8 @@ public class ProgramItemView extends TextView { public ProgramItemView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + setOnClickListener(ON_CLICKED); + setOnFocusChangeListener(ON_FOCUS_CHANGED); } private void initIfNeeded() { @@ -282,11 +247,9 @@ public class ProgramItemView extends TextView { return mTableEntry; } - public void onBind(TableEntry entry, ProgramListAdapter adapter) { + public void setValues(TableEntry entry, int selectedGenreId, long fromUtcMillis, + long toUtcMillis, String gapTitle) { mTableEntry = entry; - setOnClickListener(ON_CLICKED); - setOnFocusChangeListener(ON_FOCUS_CHANGED); - ProgramManager programManager = adapter.getProgramManager(); ViewGroup.LayoutParams layoutParams = getLayoutParams(); layoutParams.width = entry.getWidth(); @@ -303,16 +266,19 @@ public class ProgramItemView extends TextView { setText(null); } else { if (entry.isGap()) { - if (entry.isBlocked()) { - title = adapter.getBlockedProgramTitle(); - } else { - title = adapter.getNoInfoProgramTitle(); - } + title = gapTitle; episode = null; - } else if (entry.hasGenre(programManager.getSelectedGenreId())) { + } else if (entry.hasGenre(selectedGenreId)) { titleStyle = sProgramTitleStyle; episodeStyle = sEpisodeTitleStyle; } + if (TextUtils.isEmpty(title)) { + title = getResources().getString(R.string.program_title_for_no_information); + } + if (mTableEntry.scheduledRecording != null) { + //TODO(dvr): use a proper icon for UI status. + title = "®" + title; + } SpannableStringBuilder description = new SpannableStringBuilder(); description.append(title); @@ -340,12 +306,11 @@ public class ProgramItemView extends TextView { measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); mTextWidth = getMeasuredWidth() - getPaddingStart() - getPaddingEnd(); int start = GuideUtils.convertMillisToPixel(entry.entryStartUtcMillis); - int guideStart = GuideUtils.convertMillisToPixel(programManager.getFromUtcMillis()); + int guideStart = GuideUtils.convertMillisToPixel(fromUtcMillis); layoutVisibleArea(guideStart - start); // Maximum width for us to use a ripple - mMaxWidthForRipple = GuideUtils.convertMillisToPixel( - programManager.getFromUtcMillis(), programManager.getToUtcMillis()); + mMaxWidthForRipple = GuideUtils.convertMillisToPixel(fromUtcMillis, toUtcMillis); } /** @@ -374,14 +339,13 @@ public class ProgramItemView extends TextView { } } - public void onUnbind() { + public void clearValues() { if (getHandler() != null) { getHandler().removeCallbacks(mUpdateFocus); } setTag(null); - setOnFocusChangeListener(null); - setOnClickListener(null); + mTableEntry = null; } private static int getProgress(long start, long end) { diff --git a/src/com/android/tv/guide/ProgramListAdapter.java b/src/com/android/tv/guide/ProgramListAdapter.java index 88ba435e..03aea5ad 100644 --- a/src/com/android/tv/guide/ProgramListAdapter.java +++ b/src/com/android/tv/guide/ProgramListAdapter.java @@ -16,7 +16,6 @@ package com.android.tv.guide; -import android.content.Context; import android.content.res.Resources; import android.support.v7.widget.RecyclerView; import android.util.Log; @@ -33,30 +32,24 @@ import com.android.tv.guide.ProgramManager.TableEntry; * Adapts a program list for a specific channel from {@link ProgramManager} to a row of the program * guide table. */ -public class ProgramListAdapter extends - RecyclerView.Adapter<ProgramListAdapter.ProgramViewHolder> implements - TableEntriesUpdatedListener { +public class ProgramListAdapter extends RecyclerView.Adapter<ProgramListAdapter.ProgramViewHolder> + implements TableEntriesUpdatedListener { private static final String TAG = "ProgramListAdapter"; private static final boolean DEBUG = false; - private final String mNoInfoProgramTitle; - private final String mBlockedProgramTitle; - private final ProgramManager mProgramManager; private final int mChannelIndex; + private final String mNoInfoProgramTitle; + private final String mBlockedProgramTitle; private long mChannelId; - public ProgramListAdapter(Context context, ProgramManager programManager, - int channelIndex) { - Resources res = context.getResources(); - mNoInfoProgramTitle = res.getString( - R.string.program_title_for_no_information); - mBlockedProgramTitle = res.getString( - R.string.program_title_for_blocked_channel); - + public ProgramListAdapter(Resources res, ProgramManager programManager, int channelIndex) { + setHasStableIds(true); mProgramManager = programManager; mChannelIndex = channelIndex; + mNoInfoProgramTitle = res.getString(R.string.program_title_for_no_information); + mBlockedProgramTitle = res.getString(R.string.program_title_for_blocked_channel); onTableEntriesUpdated(); } @@ -76,14 +69,6 @@ public class ProgramListAdapter extends return mProgramManager; } - public String getNoInfoProgramTitle() { - return mNoInfoProgramTitle; - } - - public String getBlockedProgramTitle() { - return mBlockedProgramTitle; - } - @Override public int getItemCount() { return mProgramManager.getTableEntryCount(mChannelId); @@ -95,8 +80,15 @@ public class ProgramListAdapter extends } @Override + public long getItemId(int position) { + return mProgramManager.getTableEntry(mChannelId, position).getId(); + } + + @Override public void onBindViewHolder(ProgramViewHolder holder, int position) { - holder.onBind(mProgramManager.getTableEntry(mChannelId, position), this); + TableEntry tableEntry = mProgramManager.getTableEntry(mChannelId, position); + String gapTitle = tableEntry.isBlocked() ? mBlockedProgramTitle : mNoInfoProgramTitle; + holder.onBind(tableEntry, this.getProgramManager(), gapTitle); } @Override @@ -116,16 +108,16 @@ public class ProgramListAdapter extends super(itemView); } - public void onBind(TableEntry entry, ProgramListAdapter adapter) { + public void onBind(TableEntry entry, ProgramManager programManager, String gapTitle) { if (DEBUG) { Log.d(TAG, "onBind. View = " + itemView + ", Entry = " + entry); } - - ((ProgramItemView) itemView).onBind(entry, adapter); + ((ProgramItemView) itemView).setValues(entry, programManager.getSelectedGenreId(), + programManager.getFromUtcMillis(), programManager.getToUtcMillis(), gapTitle); } public void onUnbind() { - ((ProgramItemView) itemView).onUnbind(); + ((ProgramItemView) itemView).clearValues(); } } } diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java index df52abbe..fe1a981f 100644 --- a/src/com/android/tv/guide/ProgramManager.java +++ b/src/com/android/tv/guide/ProgramManager.java @@ -17,14 +17,17 @@ package com.android.tv.guide; import android.support.annotation.MainThread; +import android.support.annotation.Nullable; +import android.util.ArraySet; import android.util.Log; -import com.android.tv.common.CollectionUtils; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.GenreItems; import com.android.tv.data.Program; import com.android.tv.data.ProgramDataManager; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -55,6 +58,7 @@ public class ProgramManager { private final TvInputManagerHelper mTvInputManagerHelper; private final ChannelDataManager mChannelDataManager; private final ProgramDataManager mProgramDataManager; + private final DvrDataManager mDvrDataManager; // Only set if DVR is enabled private long mStartUtcMillis; private long mEndUtcMillis; @@ -74,6 +78,8 @@ public class ProgramManager { /** Program corresponding to the entry. {@code null} means that this entry is a gap. */ public final Program program; + public final ScheduledRecording scheduledRecording; + /** Start time of entry in UTC milliseconds. */ public final long entryStartUtcMillis; @@ -82,34 +88,39 @@ public class ProgramManager { private final boolean mIsBlocked; - private TableEntry(long startUtcMillis, long endUtcMillis) { - this(INVALID_ID, null, startUtcMillis, endUtcMillis, false); - } - private TableEntry(long channelId, long startUtcMillis, long endUtcMillis) { this(channelId, null, startUtcMillis, endUtcMillis, false); } private TableEntry(long channelId, long startUtcMillis, long endUtcMillis, boolean blocked) { - this(channelId, null, startUtcMillis, endUtcMillis, blocked); + this(channelId, null, null, startUtcMillis, endUtcMillis, blocked); } - private TableEntry(long channelId, Program program, - long entryStartUtcMillis, long entryEndUtcMillis) { - this(channelId, program, entryStartUtcMillis, entryEndUtcMillis, false); + private TableEntry(long channelId, Program program, long entryStartUtcMillis, + long entryEndUtcMillis, boolean isBlocked) { + this(channelId, program, null, entryStartUtcMillis, entryEndUtcMillis, isBlocked); } - private TableEntry(long channelId, Program program, + private TableEntry(long channelId, Program program, ScheduledRecording scheduledRecording, long entryStartUtcMillis, long entryEndUtcMillis, boolean isBlocked) { this.channelId = channelId; this.program = program; + this.scheduledRecording = scheduledRecording; this.entryStartUtcMillis = entryStartUtcMillis; this.entryEndUtcMillis = entryEndUtcMillis; mIsBlocked = isBlocked; } /** + * A stable id useful for {@link android.support.v7.widget.RecyclerView.Adapter}. + */ + public long getId() { + // using a negative entryEndUtcMillis keeps it from conflicting with program Id + return program != null ? program.getId() : -entryEndUtcMillis; + } + + /** * Returns true if this is a gap. */ public boolean isGap() { @@ -167,9 +178,10 @@ public class ProgramManager { // Should be matched with mSelectedGenreId always. private List<Channel> mFilteredChannels = mChannels; - private final Set<Listener> mListeners = CollectionUtils.createSmallSet(); - private final Set<TableEntriesUpdatedListener> mTableEntriesUpdatedListeners = CollectionUtils - .createSmallSet(); + private final Set<Listener> mListeners = new ArraySet<>(); + private final Set<TableEntriesUpdatedListener> mTableEntriesUpdatedListeners = new ArraySet<>(); + + private final Set<TableEntryChangedListener> mTableEntryChangedListeners = new ArraySet<>(); private final ChannelDataManager.Listener mChannelDataManagerListener = new ChannelDataManager.Listener() { @@ -197,12 +209,49 @@ public class ProgramManager { } }; + private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener = + new DvrDataManager.ScheduledRecordingListener() { + @Override + public void onScheduledRecordingAdded(ScheduledRecording scheduledRecording) { + TableEntry oldEntry = getTableEntry(scheduledRecording); + if (oldEntry != null) { + TableEntry newEntry = new TableEntry(oldEntry.channelId, oldEntry.program, + scheduledRecording, oldEntry.entryStartUtcMillis, + oldEntry.entryEndUtcMillis, oldEntry.isBlocked()); + updateEntry(oldEntry, newEntry); + } + } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording scheduledRecording) { + TableEntry oldEntry = getTableEntry(scheduledRecording); + if (oldEntry != null) { + TableEntry newEntry = new TableEntry(oldEntry.channelId, oldEntry.program, null, + oldEntry.entryStartUtcMillis, oldEntry.entryEndUtcMillis, + oldEntry.isBlocked()); + updateEntry(oldEntry, newEntry); + } + } + + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording scheduledRecording) { + TableEntry oldEntry = getTableEntry(scheduledRecording); + if (oldEntry != null) { + TableEntry newEntry = new TableEntry(oldEntry.channelId, oldEntry.program, + scheduledRecording, oldEntry.entryStartUtcMillis, + oldEntry.entryEndUtcMillis, oldEntry.isBlocked()); + updateEntry(oldEntry, newEntry); + } + } + }; + public ProgramManager(TvInputManagerHelper tvInputManagerHelper, - ChannelDataManager channelDataManager, - ProgramDataManager programDataManager) { + ChannelDataManager channelDataManager, ProgramDataManager programDataManager, + @Nullable DvrDataManager dvrDataManager) { mTvInputManagerHelper = tvInputManagerHelper; mChannelDataManager = channelDataManager; mProgramDataManager = programDataManager; + mDvrDataManager = dvrDataManager; } public void programGuideVisibilityChanged(boolean visible) { @@ -210,41 +259,61 @@ public class ProgramManager { if (visible) { mChannelDataManager.addListener(mChannelDataManagerListener); mProgramDataManager.addListener(mProgramDataManagerListener); + if (mDvrDataManager != null) { + mDvrDataManager.addScheduledRecordingListener(mScheduledRecordingListener); + } } else { mChannelDataManager.removeListener(mChannelDataManagerListener); mProgramDataManager.removeListener(mProgramDataManagerListener); + if (mDvrDataManager != null) { + mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener); + } } } /** - * Add a {@link Listener}. + * Adds a {@link Listener}. */ public void addListener(Listener listener) { mListeners.add(listener); } /** - * Register a listener to be invoked when table entries are updated. + * Registers a listener to be invoked when table entries are updated. */ public void addTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) { mTableEntriesUpdatedListeners.add(listener); } /** - * Remove a {@link Listener}. + * Registers a listener to be invoked when a table entry is changed. + */ + public void addTableEntryChangedListener(TableEntryChangedListener listener) { + mTableEntryChangedListeners.add(listener); + } + + /** + * Removes a {@link Listener}. */ public void removeListener(Listener listener) { mListeners.remove(listener); } /** - * Remove a previously installed table entries update listener. + * Removes a previously installed table entries update listener. */ public void removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) { mTableEntriesUpdatedListeners.remove(listener); } /** + * Removes a previously installed table entry changed listener. + */ + public void removeTableEntryChangedListener(TableEntryChangedListener listener) { + mTableEntryChangedListeners.remove(listener); + } + + /** * Build genre filters based on the current programs. * This categories channels by its current program's canonical genres * and subsequent @{link resetChannelListWithGenre(int)} calls will reset channel list @@ -366,6 +435,7 @@ public class ProgramManager { } else if (lastEntry.entryEndUtcMillis == Long.MAX_VALUE) { entries.remove(entries.size() - 1); entries.add(new TableEntry(lastEntry.channelId, lastEntry.program, + lastEntry.scheduledRecording, lastEntry.entryStartUtcMillis, mEndUtcMillis, lastEntry.mIsBlocked)); } @@ -403,6 +473,37 @@ public class ProgramManager { } } + private void notifyTableEntryUpdated(TableEntry entry) { + for (TableEntryChangedListener listener : mTableEntryChangedListeners) { + listener.onTableEntryChanged(entry); + } + } + + private void updateEntry(TableEntry old, TableEntry newEntry) { + List<TableEntry> entries = mChannelIdEntriesMap.get(old.channelId); + int index = entries.indexOf(old); + entries.set(index, newEntry); + notifyTableEntryUpdated(newEntry); + } + + @Nullable + private TableEntry getTableEntry(ScheduledRecording scheduledRecording) { + return getTableEntry(scheduledRecording.getChannelId(), scheduledRecording.getProgramId()); + } + + @Nullable + private TableEntry getTableEntry(long channelId, long entryId) { + List<TableEntry> entries = mChannelIdEntriesMap.get(channelId); + if (entries != null) { + for (TableEntry entry : entries) { + if (entry.getId() == entryId) { + return entry; + } + } + } + return null; + } + /** * Returns the start time of currently managed time range, in UTC millisecond. */ @@ -471,6 +572,14 @@ public class ProgramManager { } /** + * Returns the index of channel with {@code channelId} within the currently managed channels. + * Returns -1 if such a channel is not found. + */ + public int getChannelIndex(long channelId) { + return getChannelIndex(mChannelDataManager.getChannel(channelId)); + } + + /** * Returns the number of "entries", which lies within the currently managed time range, for a * given {@code channelId}. */ @@ -511,8 +620,10 @@ public class ProgramManager { lastProgramEndTime = programStartTime; } if (programEndTime > lastProgramEndTime) { - entries.add(new TableEntry(channelId, program, lastProgramEndTime, - programEndTime)); + ScheduledRecording scheduledRecording = mDvrDataManager == null ? null + : mDvrDataManager.getScheduledRecordingForProgramId(program.getId()); + entries.add(new TableEntry(channelId, program, scheduledRecording, + lastProgramEndTime, programEndTime, false)); lastProgramEndTime = programEndTime; } } @@ -525,7 +636,8 @@ public class ProgramManager { // the first entry from UI perspective. So we clip it out. entries.remove(0); entries.set(0, new TableEntry(secondEntry.channelId, secondEntry.program, - mStartUtcMillis, secondEntry.entryEndUtcMillis)); + secondEntry.scheduledRecording, mStartUtcMillis, + secondEntry.entryEndUtcMillis, secondEntry.mIsBlocked)); } } return entries; @@ -555,6 +667,10 @@ public class ProgramManager { void onTableEntriesUpdated(); } + public interface TableEntryChangedListener { + void onTableEntryChanged(TableEntry entry); + } + public static class ListenerAdapter implements Listener { @Override public void onGenresUpdated() { } @@ -598,9 +714,24 @@ public class ProgramManager { } /** - * Returns the program index of the program at {@code time}. + * Returns the program index of the program with {@code entryId} or -1 if not found. + */ + public int getProgramIdIndex(long channelId, long entryId) { + List<TableEntry> entries = mChannelIdEntriesMap.get(channelId); + if (entries != null) { + for (int i = 0; i < entries.size(); i++) { + if (entries.get(i).getId() == entryId) { + return i; + } + } + } + return -1; + } + + /** + * Returns the program index of the program at {@code time} or -1 if not found. */ - public int getProgramIndex(long channelId, long time) { + public int getProgramIndexAtTime(long channelId, long time) { List<TableEntry> entries = mChannelIdEntriesMap.get(channelId); for (int i = 0; i < entries.size(); ++i) { TableEntry entry = entries.get(i); diff --git a/src/com/android/tv/guide/ProgramRow.java b/src/com/android/tv/guide/ProgramRow.java index 4f38b879..54b864db 100644 --- a/src/com/android/tv/guide/ProgramRow.java +++ b/src/com/android/tv/guide/ProgramRow.java @@ -306,7 +306,7 @@ public class ProgramRow extends TimelineGridView { public void resetScroll(int scrollOffset) { long startTime = GuideUtils.convertPixelToMillis(scrollOffset) + mProgramManager.getStartTime(); - int position = mChannel == null ? -1 : mProgramManager.getProgramIndex( + int position = mChannel == null ? -1 : mProgramManager.getProgramIndexAtTime( mChannel.getId(), startTime); if (position < 0) { getLayoutManager().scrollToPosition(0); diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java index a86c1332..83755b5f 100644 --- a/src/com/android/tv/guide/ProgramTableAdapter.java +++ b/src/com/android/tv/guide/ProgramTableAdapter.java @@ -26,7 +26,9 @@ import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Bitmap; import android.media.tv.TvContentRating; +import android.media.tv.TvInputInfo; import android.os.Handler; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.RecycledViewPool; @@ -43,11 +45,15 @@ import android.widget.ImageView; import android.widget.TextView; import com.android.tv.R; +import com.android.tv.TvApplication; import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.guide.ProgramManager.TableEntriesUpdatedListener; import com.android.tv.parental.ParentalControlSettings; import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; +import com.android.tv.util.ImageCache; +import com.android.tv.util.ImageLoader; +import com.android.tv.util.ImageLoader.LoadTvInputLogoTask; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -57,8 +63,8 @@ import java.util.List; /** * Adapts the {@link ProgramListAdapter} list to the body of the program guide table. */ -public class ProgramTableAdapter extends - RecyclerView.Adapter<ProgramTableAdapter.ProgramRowHolder> { +public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.ProgramRowHolder> + implements ProgramManager.TableEntryChangedListener { private static final String TAG = "ProgramTableAdapter"; private static final boolean DEBUG = false; @@ -84,10 +90,10 @@ public class ProgramTableAdapter extends private final int mDetailPadding; private final TextAppearanceSpan mEpisodeTitleStyle; - public ProgramTableAdapter(Context context, TvInputManagerHelper tvInputManagerHelper, - ProgramManager programManager, ProgramGuide programGuide) { + public ProgramTableAdapter(Context context, ProgramManager programManager, + ProgramGuide programGuide) { mContext = context; - mTvInputManagerHelper = tvInputManagerHelper; + mTvInputManagerHelper = TvApplication.getSingletons(context).getTvInputManagerHelper(); mProgramManager = programManager; mProgramGuide = programGuide; @@ -140,6 +146,7 @@ public class ProgramTableAdapter extends } }); update(); + mProgramManager.addTableEntryChangedListener(this); } private void update() { @@ -149,7 +156,8 @@ public class ProgramTableAdapter extends } mProgramListAdapters.clear(); for (int i = 0; i < mProgramManager.getChannelCount(); i++) { - ProgramListAdapter listAdapter = new ProgramListAdapter(mContext, mProgramManager, i); + ProgramListAdapter listAdapter = new ProgramListAdapter(mContext.getResources(), + mProgramManager, i); mProgramManager.addTableEntriesUpdatedListener(listAdapter); mProgramListAdapters.add(listAdapter); } @@ -179,6 +187,14 @@ public class ProgramTableAdapter extends return new ProgramRowHolder(itemView); } + @Override + public void onTableEntryChanged(ProgramManager.TableEntry tableEntry) { + int channelIndex = mProgramManager.getChannelIndex(tableEntry.channelId); + int pos = mProgramManager.getProgramIdIndex(tableEntry.channelId, tableEntry.getId()); + if (DEBUG) Log.d(TAG, "update(" + channelIndex + ", " + pos + ")"); + mProgramListAdapters.get(channelIndex).notifyItemChanged(pos, tableEntry); + } + // TODO: make it static public class ProgramRowHolder extends RecyclerView.ViewHolder implements ProgramRow.ChildFocusListener { @@ -223,6 +239,9 @@ public class ProgramTableAdapter extends private final TextView mChannelNameView; private final ImageView mChannelLogoView; private final ImageView mChannelBlockView; + private final ImageView mInputLogoView; + + private boolean mIsInputLogoVisible; public ProgramRowHolder(View itemView) { super(itemView); @@ -244,6 +263,7 @@ public class ProgramTableAdapter extends mChannelNameView = (TextView) mContainer.findViewById(R.id.channel_name); mChannelLogoView = (ImageView) mContainer.findViewById(R.id.channel_logo); mChannelBlockView = (ImageView) mContainer.findViewById(R.id.channel_block); + mInputLogoView = (ImageView) mContainer.findViewById(R.id.input_logo); } public void onBind(int position) { @@ -267,6 +287,8 @@ public class ProgramTableAdapter extends if (DEBUG) Log.d(TAG, "onBindChannel " + channel); mChannel = channel; + mInputLogoView.setVisibility(View.GONE); + mIsInputLogoVisible = false; if (channel == null) { mChannelNumberView.setVisibility(View.GONE); mChannelNameView.setVisibility(View.GONE); @@ -467,6 +489,43 @@ public class ProgramTableAdapter extends } } + /** + * Update tv input logo. It should be called when the visible child item in ProgramGrid + * changed. + */ + public void updateInputLogo(int lastPosition, boolean forceShow) { + if (mChannel == null) { + mInputLogoView.setVisibility(View.GONE); + mIsInputLogoVisible = false; + return; + } + + boolean showLogo = forceShow; + if (!showLogo) { + Channel lastChannel = mProgramManager.getChannel(lastPosition); + if (lastChannel == null + || !mChannel.getInputId().equals(lastChannel.getInputId())) { + showLogo = true; + } + } + + if (showLogo) { + if (!mIsInputLogoVisible) { + mIsInputLogoVisible = true; + TvInputInfo info = mTvInputManagerHelper.getTvInputInfo(mChannel.getInputId()); + if (info != null) { + LoadTvInputLogoTask task = new LoadTvInputLogoTask( + itemView.getContext(), ImageCache.getInstance(), info); + ImageLoader.loadBitmap(createTvInputLogoLoadedCallback(info, this), task); + } + } + } else { + mInputLogoView.setVisibility(View.GONE); + mInputLogoView.setImageDrawable(null); + mIsInputLogoVisible = false; + } + } + private void updateTextView(TextView textView, String text) { if (!TextUtils.isEmpty(text)) { textView.setVisibility(View.VISIBLE); @@ -487,6 +546,14 @@ public class ProgramTableAdapter extends mChannelLogoView.setVisibility(View.VISIBLE); } + private void updateInputLogoInternal(@NonNull Bitmap tvInputLogo) { + if (!mIsInputLogoVisible) { + return; + } + mInputLogoView.setImageBitmap(tvInputLogo); + mInputLogoView.setVisibility(View.VISIBLE); + } + private void onHorizontalScrolled() { if (mDetailInAnimator != null) { mHandler.removeCallbacks(mDetailInStarter); @@ -526,4 +593,17 @@ public class ProgramTableAdapter extends } }; } + + private static ImageLoaderCallback<ProgramRowHolder> createTvInputLogoLoadedCallback( + final TvInputInfo info, ProgramRowHolder holder) { + return new ImageLoaderCallback<ProgramRowHolder>(holder) { + @Override + public void onBitmapLoaded(ProgramRowHolder holder, @Nullable Bitmap logo) { + if (logo != null && info.getId() + .equals(holder.mChannel.getInputId())) { + holder.updateInputLogoInternal(logo); + } + } + }; + } } diff --git a/src/com/android/tv/guide/TimelineRow.java b/src/com/android/tv/guide/TimelineRow.java index 891b14cd..3f0c8678 100644 --- a/src/com/android/tv/guide/TimelineRow.java +++ b/src/com/android/tv/guide/TimelineRow.java @@ -64,7 +64,9 @@ public class TimelineRow extends TimelineGridView { public void onRtlPropertiesChanged(int layoutDirection) { super.onRtlPropertiesChanged(layoutDirection); // Reset scroll - scrollTo(getScrollOffset(), false); + if (isAttachedToWindow()) { + scrollTo(getScrollOffset(), false); + } } @Override diff --git a/src/com/android/tv/menu/ActionCardView.java b/src/com/android/tv/menu/ActionCardView.java index 1848a3ce..2d72b06f 100644 --- a/src/com/android/tv/menu/ActionCardView.java +++ b/src/com/android/tv/menu/ActionCardView.java @@ -93,4 +93,7 @@ public class ActionCardView extends FrameLayout implements ItemListRowView.CardV Log.d(TAG, "onDeselected: action=" + mLabelView.getText()); } } + + @Override + public void onRecycled() { } } diff --git a/src/com/android/tv/menu/BaseCardView.java b/src/com/android/tv/menu/BaseCardView.java index 25d4e313..b4500dd1 100644 --- a/src/com/android/tv/menu/BaseCardView.java +++ b/src/com/android/tv/menu/BaseCardView.java @@ -85,6 +85,9 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo } @Override + public void onRecycled() { } + + @Override public void onSelected() { if (isAttachedToWindow() && getVisibility() == View.VISIBLE) { startFocusAnimation(SCALE_FACTOR_1F); diff --git a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java index 1e416e5b..f932d75d 100644 --- a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java +++ b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java @@ -25,11 +25,11 @@ import android.support.annotation.NonNull; import android.util.Log; import com.android.tv.R; +import com.android.tv.common.SoftPreconditions; import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.data.ProgramDataManager; -import com.android.tv.util.SoftPreconditions; import java.util.List; diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java index 51867d0b..200f4ac0 100644 --- a/src/com/android/tv/menu/ChannelsRowAdapter.java +++ b/src/com/android/tv/menu/ChannelsRowAdapter.java @@ -18,7 +18,9 @@ package com.android.tv.menu; import android.content.Context; import android.content.Intent; +import android.media.tv.TvInputInfo; import android.os.Build; +import android.support.v4.os.BuildCompat; import android.view.View; import com.android.tv.MainActivity; @@ -29,6 +31,8 @@ import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; import com.android.tv.recommendation.Recommender; import com.android.tv.util.SetupUtils; +import com.android.tv.util.TvInputManagerHelper; +import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.List; @@ -37,16 +41,16 @@ import java.util.List; * An adapter of the Channels row. */ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel> { - // There are four special cards: guide, setup, dvr, applink. - private static final int SIZE_OF_VIEW_TYPE = 4; + // There are four special cards: guide, setup, dvr, record, applink. + private static final int SIZE_OF_VIEW_TYPE = 5; private final Context mContext; private final Tracker mTracker; private final Recommender mRecommender; private final int mMaxCount; private final int mMinCount; - private boolean mShowDvrCard; - private int[] mViewType = new int[SIZE_OF_VIEW_TYPE]; + private final boolean mDvrFeatureEnabled; + private final int[] mViewType = new int[SIZE_OF_VIEW_TYPE]; private final View.OnClickListener mGuideOnClickListener = new View.OnClickListener() { @Override @@ -67,11 +71,28 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel> private final View.OnClickListener mDvrOnClickListener = new View.OnClickListener() { @Override public void onClick(View view) { + Utils.showToastMessageForDeveloperFeature(view.getContext()); mTracker.sendMenuClicked(R.string.channels_item_dvr); getMainActivity().getOverlayManager().showDvrManager(); } }; + private final View.OnClickListener mRecordOnClickListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + Utils.showToastMessageForDeveloperFeature(view.getContext()); + RecordCardView v = ((RecordCardView) view); + boolean isRecording = v.isRecording(); + mTracker.sendMenuClicked(isRecording ? R.string.channels_item_record_start + : R.string.channels_item_record_stop); + if (!isRecording) { + v.startRecording(); + } else { + v.stopRecording(); + } + } + }; + private final View.OnClickListener mAppLinkOnClickListener = new View.OnClickListener() { @Override public void onClick(View view) { @@ -102,7 +123,7 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel> mRecommender = recommender; mMinCount = minCount; mMaxCount = maxCount; - mShowDvrCard = CommonFeatures.DVR.isEnabled(mContext); + mDvrFeatureEnabled = CommonFeatures.DVR.isEnabled(mContext) && BuildCompat.isAtLeastN(); } @Override @@ -131,6 +152,8 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel> viewHolder.itemView.setOnClickListener(mAppLinkOnClickListener); } else if (viewType == R.layout.menu_card_dvr) { viewHolder.itemView.setOnClickListener(mDvrOnClickListener); + } else if (viewType == R.layout.menu_card_record) { + viewHolder.itemView.setOnClickListener(mRecordOnClickListener); } else { viewHolder.itemView.setTag(getItemList().get(position)); viewHolder.itemView.setOnClickListener(mChannelOnClickListener); @@ -140,17 +163,33 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel> @Override public void update() { List<Channel> channelList = new ArrayList<>(); - Channel dummyChannel = new Channel.Builder() - .build(); + Channel dummyChannel = new Channel.Builder().build(); // For guide item channelList.add(dummyChannel); // For setup item - boolean showSetupCard = SetupUtils.getInstance(mContext) - .hasNewInput(((MainActivity) mContext).getTvInputManagerHelper()); + TvInputManagerHelper inputManager = TvApplication.getSingletons(mContext) + .getTvInputManagerHelper(); + boolean showSetupCard = SetupUtils.getInstance(mContext).hasNewInput(inputManager); Channel currentChannel = ((MainActivity) mContext).getCurrentChannel(); boolean showAppLinkCard = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && currentChannel != null && currentChannel.getAppLinkType(mContext) != Channel.APP_LINK_TYPE_NONE; + boolean showDvrCard = false; + boolean showRecordCard = false; + if (mDvrFeatureEnabled) { + for (TvInputInfo info : inputManager.getTvInputInfos(true, true)) { + if (info.canRecord()) { + showDvrCard = true; + break; + } + } + if (currentChannel != null && currentChannel.getInputId() != null) { + TvInputInfo inputInfo = inputManager.getTvInputInfo(currentChannel.getInputId()); + if ((inputInfo.canRecord() && inputInfo.getTunerCount() > 1)) { + showRecordCard = true; + } + } + } mViewType[0] = R.layout.menu_card_guide; int index = 1; @@ -158,10 +197,14 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel> channelList.add(dummyChannel); mViewType[index++] = R.layout.menu_card_setup; } - if (mShowDvrCard) { + if (showDvrCard) { channelList.add(dummyChannel); mViewType[index++] = R.layout.menu_card_dvr; } + if (showRecordCard) { + channelList.add(currentChannel); + mViewType[index++] = R.layout.menu_card_record; + } if (showAppLinkCard) { channelList.add(currentChannel); mViewType[index++] = R.layout.menu_card_app_link; diff --git a/src/com/android/tv/menu/ItemListRowView.java b/src/com/android/tv/menu/ItemListRowView.java index e9362a78..4919c595 100644 --- a/src/com/android/tv/menu/ItemListRowView.java +++ b/src/com/android/tv/menu/ItemListRowView.java @@ -41,6 +41,7 @@ public class ItemListRowView extends MenuRowView implements OnChildSelectedListe public interface CardView<T> { void onBind(T row, boolean selected); + void onRecycled(); void onSelected(); void onDeselected(); } @@ -206,6 +207,13 @@ public class ItemListRowView extends MenuRowView implements OnChildSelectedListe cardView.onBind(mItemList.get(position), cardView.equals(mItemListView.mSelectedCard)); } + @Override + public void onViewRecycled(MyViewHolder viewHolder) { + super.onViewRecycled(viewHolder); + CardView<T> cardView = (CardView<T>) viewHolder.itemView; + cardView.onRecycled(); + } + public static class MyViewHolder extends RecyclerView.ViewHolder { public MyViewHolder(View view) { super(view); diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java index 613e0d62..7bb0787e 100644 --- a/src/com/android/tv/menu/Menu.java +++ b/src/com/android/tv/menu/Menu.java @@ -56,7 +56,7 @@ public class Menu { @IntDef({REASON_NONE, REASON_GUIDE, REASON_PLAY_CONTROLS_PLAY, REASON_PLAY_CONTROLS_PAUSE, REASON_PLAY_CONTROLS_PLAY_PAUSE, REASON_PLAY_CONTROLS_REWIND, REASON_PLAY_CONTROLS_FAST_FORWARD, REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS, - REASON_PLAY_CONTROLS_JUMP_TO_NEXT}) + REASON_PLAY_CONTROLS_JUMP_TO_NEXT, REASON_RECORDING_PLAYBACK}) public @interface MenuShowReason {} public static final int REASON_NONE = 0; public static final int REASON_GUIDE = 1; @@ -67,6 +67,7 @@ public class Menu { public static final int REASON_PLAY_CONTROLS_FAST_FORWARD = 6; public static final int REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS = 7; public static final int REASON_PLAY_CONTROLS_JUMP_TO_NEXT = 8; + public static final int REASON_RECORDING_PLAYBACK = 9; private static final List<String> sRowIdListForReason = new ArrayList<>(); static { @@ -79,6 +80,7 @@ public class Menu { sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_FAST_FORWARD sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_JUMP_TO_NEXT + sRowIdListForReason.add(PlayControlsRow.ID); // REASON_RECORDING_PLAYBACK } private static final String SCREEN_NAME = "Menu"; diff --git a/src/com/android/tv/menu/MenuAction.java b/src/com/android/tv/menu/MenuAction.java index b45e88c2..86153084 100644 --- a/src/com/android/tv/menu/MenuAction.java +++ b/src/com/android/tv/menu/MenuAction.java @@ -36,8 +36,11 @@ public class MenuAction { public static final MenuAction SELECT_DISPLAY_MODE_ACTION = new MenuAction(R.string.options_item_display_mode, TvOptionsManager.OPTION_DISPLAY_MODE, R.drawable.ic_tvoption_aspect); - public static final MenuAction PIP_ACTION = - new MenuAction(R.string.options_item_pip, TvOptionsManager.OPTION_PIP, + public static final MenuAction PIP_IN_APP_ACTION = + new MenuAction(R.string.options_item_pip, TvOptionsManager.OPTION_IN_APP_PIP, + R.drawable.ic_tvoption_pip); + public static final MenuAction SYSTEMWIDE_PIP_ACTION = + new MenuAction(R.string.options_item_pip, TvOptionsManager.OPTION_SYSTEMWIDE_PIP, R.drawable.ic_tvoption_pip); public static final MenuAction SELECT_AUDIO_LANGUAGE_ACTION = new MenuAction(R.string.options_item_multi_audio, TvOptionsManager.OPTION_MULTI_AUDIO, diff --git a/src/com/android/tv/menu/MenuLayoutManager.java b/src/com/android/tv/menu/MenuLayoutManager.java index 265ad840..1f377f54 100644 --- a/src/com/android/tv/menu/MenuLayoutManager.java +++ b/src/com/android/tv/menu/MenuLayoutManager.java @@ -35,7 +35,7 @@ import android.view.ViewGroup.MarginLayoutParams; import android.widget.TextView; import com.android.tv.R; -import com.android.tv.util.SoftPreconditions; +import com.android.tv.common.SoftPreconditions; import com.android.tv.util.Utils; import java.util.ArrayList; @@ -318,6 +318,11 @@ public class MenuLayoutManager { if (!indexValid) { return; } + MenuRow row = mMenuRows.get(position); + if (!row.isVisible()) { + Log.e(TAG, "Selecting invisible row: " + position); + return; + } if (Utils.isIndexValid(mMenuRowViews, mSelectedPosition)) { mMenuRowViews.get(mSelectedPosition).onDeselected(); } @@ -360,6 +365,11 @@ public class MenuLayoutManager { if (!newIndexValid) { return; } + MenuRow row = mMenuRows.get(position); + if (!row.isVisible()) { + Log.e(TAG, "Moving to the invisible row: " + position); + return; + } if (mAnimatorSet != null) { // Do not cancel the animation here. The property values should be set to the end values // when the animation finishes. @@ -787,9 +797,9 @@ public class MenuLayoutManager { } private static final class ViewPropertyValueHolder { - public Property<View, Float> property; - public View view; - public float value; + public final Property<View, Float> property; + public final View view; + public final float value; public ViewPropertyValueHolder(Property<View, Float> property, View view, float value) { this.property = property; diff --git a/src/com/android/tv/menu/MenuView.java b/src/com/android/tv/menu/MenuView.java index df91ddf3..e012dfca 100644 --- a/src/com/android/tv/menu/MenuView.java +++ b/src/com/android/tv/menu/MenuView.java @@ -117,10 +117,11 @@ public class MenuView extends FrameLayout implements IMenuView { } initializeChildren(); update(true); - if (rowIdToSelect == null) { - rowIdToSelect = ChannelsRow.ID; - } int position = getItemPosition(rowIdToSelect); + if (position == -1 || !mMenuRows.get(position).isVisible()) { + // Channels row is always visible. + position = getItemPosition(ChannelsRow.ID); + } setSelectedPosition(position); // Change the visibility as late as possible to avoid the unnecessary animation. setVisibility(VISIBLE); diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java index f0853c40..058d5108 100644 --- a/src/com/android/tv/menu/PlayControlsRowView.java +++ b/src/com/android/tv/menu/PlayControlsRowView.java @@ -19,6 +19,7 @@ package com.android.tv.menu; import android.content.Context; import android.content.res.Resources; import android.text.format.DateFormat; +import android.text.format.DateUtils; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; @@ -27,6 +28,7 @@ import android.widget.TextView; import com.android.tv.R; import com.android.tv.TimeShiftManager; import com.android.tv.TimeShiftManager.TimeShiftActionId; +import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; import com.android.tv.menu.Menu.MenuShowReason; @@ -249,9 +251,16 @@ public class PlayControlsRowView extends MenuRowView { } private void initializeTimeline() { - Program program = mTimeShiftManager.getProgramAt(mTimeShiftManager.getCurrentPositionMs()); - mProgramStartTimeMs = program.getStartTimeUtcMillis(); - mProgramEndTimeMs = program.getEndTimeUtcMillis(); + if (mTimeShiftManager.isRecordingPlayback()) { + mProgramStartTimeMs = mTimeShiftManager.getRecordStartTimeMs(); + mProgramEndTimeMs = mTimeShiftManager.getRecordEndTimeMs(); + } else { + Program program = mTimeShiftManager.getProgramAt( + mTimeShiftManager.getCurrentPositionMs()); + mProgramStartTimeMs = program.getStartTimeUtcMillis(); + mProgramEndTimeMs = program.getEndTimeUtcMillis(); + } + SoftPreconditions.checkArgument(mProgramStartTimeMs <= mProgramEndTimeMs); } private void updateMenuVisibility() { @@ -357,14 +366,6 @@ public class PlayControlsRowView extends MenuRowView { mTimeIndicator.setVisibility(View.INVISIBLE); return; } - if (mTimeShiftManager.isPlayForRecording()) { - mProgramStartTimeMs = mTimeShiftManager.getRecordStartTimeMs(); - mProgramEndTimeMs = Math.max(mProgramStartTimeMs, - mTimeShiftManager.getRecordEndTimeMs()); - if (mProgramStartTimeMs > mProgramEndTimeMs) { - mProgramEndTimeMs = mProgramStartTimeMs; - } - } long currentPositionMs = mTimeShiftManager.getCurrentPositionMs(); ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mTimeText.getLayoutParams(); @@ -422,15 +423,18 @@ public class PlayControlsRowView extends MenuRowView { private void updateRecTimeText() { if (isEnabled()) { - mProgramStartTimeText.setVisibility(View.VISIBLE); + if (mTimeShiftManager.isRecordingPlayback()) { + mProgramStartTimeText.setVisibility(View.GONE); + } else { + mProgramStartTimeText.setVisibility(View.VISIBLE); + mProgramStartTimeText.setText(getTimeString(mProgramStartTimeMs)); + } mProgramEndTimeText.setVisibility(View.VISIBLE); + mProgramEndTimeText.setText(getTimeString(mProgramEndTimeMs)); } else { - mProgramStartTimeText.setVisibility(View.INVISIBLE); - mProgramEndTimeText.setVisibility(View.INVISIBLE); - return; + mProgramStartTimeText.setVisibility(View.GONE); + mProgramEndTimeText.setVisibility(View.GONE); } - mProgramStartTimeText.setText(getTimeString(mProgramStartTimeMs)); - mProgramEndTimeText.setText(getTimeString(mProgramEndTimeMs)); } private void updateButtons() { @@ -478,7 +482,9 @@ public class PlayControlsRowView extends MenuRowView { } private String getTimeString(long timeMs) { - return mTimeFormat.format(timeMs); + return mTimeShiftManager.isRecordingPlayback() + ? DateUtils.formatElapsedTime(timeMs / 1000) + : mTimeFormat.format(timeMs); } private int convertDurationToPixel(long duration) { diff --git a/src/com/android/tv/menu/RecordCardView.java b/src/com/android/tv/menu/RecordCardView.java new file mode 100644 index 00000000..de30894e --- /dev/null +++ b/src/com/android/tv/menu/RecordCardView.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.menu; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.util.AttributeSet; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.tv.MainActivity; +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.data.Program; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.ScheduledRecording; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * A view to render an item of TV options. + */ +public class RecordCardView extends SimpleCardView implements + DvrDataManager.ScheduledRecordingListener { + private static final String TAG = MenuView.TAG; + private static final boolean DEBUG = MenuView.DEBUG; + private static final long MIN_PROGRAM_RECORD_DURATION = TimeUnit.MINUTES.toMillis(5); + + private ImageView mIconView; + private TextView mLabelView; + private Channel mCurrentChannel; + private final DvrManager mDvrManager; + private final DvrDataManager mDvrDataManager; + private boolean mIsRecording; + private ScheduledRecording mCurrentRecording; + + public RecordCardView(Context context) { + this(context, null); + } + + public RecordCardView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RecordCardView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mDvrManager = TvApplication.getSingletons(context).getDvrManager(); + mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); + } + + @Override + public void onBind(Channel channel, boolean selected) { + super.onBind(channel, selected); + mIconView = (ImageView) findViewById(R.id.record_icon); + mLabelView = (TextView) findViewById(R.id.record_label); + mCurrentChannel = channel; + mCurrentRecording = null; + for (ScheduledRecording recording : mDvrDataManager.getStartedRecordings()) { + if (recording.getChannelId() == channel.getId()) { + mIsRecording = true; + mCurrentRecording = recording; + } + } + mDvrDataManager.addScheduledRecordingListener(this); + updateCardView(); + } + + @Override + public void onRecycled() { + super.onRecycled(); + mDvrDataManager.removeScheduledRecordingListener(this); + } + + public boolean isRecording() { + return mIsRecording; + } + + public void startRecording() { + showStartRecordingDialog(); + } + + public void stopRecording() { + mDvrManager.stopRecording(mCurrentRecording); + } + + private void updateCardView() { + if (mIsRecording) { + mIconView.setImageResource(R.drawable.ic_record_stop); + mLabelView.setText(R.string.channels_item_record_stop); + } else { + mIconView.setImageResource(R.drawable.ic_record_start); + mLabelView.setText(R.string.channels_item_record_start); + } + } + + private void showStartRecordingDialog() { + final long endOfProgram = -1; + + final List<CharSequence> items = new ArrayList<>(); + final List<Long> durations = new ArrayList<>(); + Resources res = getResources(); + items.add(res.getString(R.string.recording_start_dialog_10_min_duration)); + durations.add(TimeUnit.MINUTES.toMillis(10)); + items.add(res.getString(R.string.recording_start_dialog_30_min_duration)); + durations.add(TimeUnit.MINUTES.toMillis(30)); + items.add(res.getString(R.string.recording_start_dialog_1_hour_duration)); + durations.add(TimeUnit.HOURS.toMillis(1)); + items.add(res.getString(R.string.recording_start_dialog_3_hours_duration)); + durations.add(TimeUnit.HOURS.toMillis(3)); + + Program currenProgram = ((MainActivity) getContext()).getCurrentProgram(false); + if (currenProgram != null) { + long duration = currenProgram.getEndTimeUtcMillis() - System.currentTimeMillis(); + if (duration > MIN_PROGRAM_RECORD_DURATION) { + items.add(res.getString(R.string.recording_start_dialog_till_end_of_program)); + durations.add(duration); + } + } + + DialogInterface.OnClickListener onClickListener + = new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, int which) { + long startTime = System.currentTimeMillis(); + long endTime = System.currentTimeMillis() + durations.get(which); + mDvrManager.addSchedule(mCurrentChannel, startTime, endTime); + dialog.dismiss(); + } + }; + new AlertDialog.Builder(getContext()) + .setItems(items.toArray(new CharSequence[items.size()]), onClickListener) + .create() + .show(); + } + + @Override + public void onScheduledRecordingAdded(ScheduledRecording recording) { + } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording recording) { + if (recording.getChannelId() != mCurrentChannel.getId()) { + return; + } + if (mIsRecording) { + mIsRecording = false; + mCurrentRecording = null; + updateCardView(); + } + } + + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording recording) { + if (recording.getChannelId() != mCurrentChannel.getId()) { + return; + } + int state = recording.getState(); + if (state == ScheduledRecording.STATE_RECORDING_FAILED + || state == ScheduledRecording.STATE_RECORDING_FINISHED) { + mIsRecording = false; + mCurrentRecording = null; + updateCardView(); + } else if (state == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { + mIsRecording = true; + mCurrentRecording = recording; + updateCardView(); + } + } +} diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java index 82525456..ba84247b 100644 --- a/src/com/android/tv/menu/TvOptionsRowAdapter.java +++ b/src/com/android/tv/menu/TvOptionsRowAdapter.java @@ -19,6 +19,7 @@ package com.android.tv.menu; import android.content.Context; import android.media.tv.TvTrackInfo; import android.support.annotation.VisibleForTesting; +import android.support.v4.os.BuildCompat; import com.android.tv.Features; import com.android.tv.R; @@ -39,10 +40,13 @@ import java.util.List; */ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { private int mPositionPipAction; - private boolean mHasPipAction = true; + // If mInAppPipAction is false, system-wide PIP is used. + private boolean mInAppPipAction = true; + private final Context mContext; public TvOptionsRowAdapter(Context context, List<CustomAction> customActions) { super(context, customActions); + mContext = context; } @Override @@ -52,8 +56,8 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { setOptionChangedListener(MenuAction.SELECT_CLOSED_CAPTION_ACTION); actionList.add(MenuAction.SELECT_DISPLAY_MODE_ACTION); setOptionChangedListener(MenuAction.SELECT_DISPLAY_MODE_ACTION); - actionList.add(MenuAction.PIP_ACTION); - setOptionChangedListener(MenuAction.PIP_ACTION); + actionList.add(MenuAction.PIP_IN_APP_ACTION); + setOptionChangedListener(MenuAction.PIP_IN_APP_ACTION); mPositionPipAction = actionList.size() - 1; actionList.add(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION); setOptionChangedListener(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION); @@ -106,34 +110,39 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { // Case 1 PipInputManager pipInputManager = getMainActivity().getPipInputManager(); if (pipInputManager.getPipInputSize(false) < 2) { - if (mHasPipAction) { + if (mInAppPipAction) { removeAction(mPositionPipAction); - mHasPipAction = false; + mInAppPipAction = false; + if (BuildCompat.isAtLeastN()) { + addAction(mPositionPipAction, MenuAction.SYSTEMWIDE_PIP_ACTION); + } return true; } + return false; } else { - if (!mHasPipAction) { - addAction(mPositionPipAction, MenuAction.PIP_ACTION); - mHasPipAction = true; + if (!mInAppPipAction) { + removeAction(mPositionPipAction); + addAction(mPositionPipAction, MenuAction.PIP_IN_APP_ACTION); + mInAppPipAction = true; changed = true; } } // Case 2 boolean isPipEnabled = getMainActivity().isPipEnabled(); - boolean oldEnabled = MenuAction.PIP_ACTION.isEnabled(); + boolean oldEnabled = MenuAction.PIP_IN_APP_ACTION.isEnabled(); boolean newEnabled = pipInputManager.getPipInputSize(true) > 0; if (oldEnabled != newEnabled) { // Should not disable the item if the PIP is already turned on so that the user can // force exit it. if (newEnabled || !isPipEnabled) { - MenuAction.PIP_ACTION.setEnabled(newEnabled); + MenuAction.PIP_IN_APP_ACTION.setEnabled(newEnabled); changed = true; } } // Case 3 & 4 - we just need to update the icon. - MenuAction.PIP_ACTION.setDrawableResId( + MenuAction.PIP_IN_APP_ACTION.setDrawableResId( isPipEnabled ? R.drawable.ic_tvoption_pip : R.drawable.ic_tvoption_pip_off); return changed; } @@ -173,9 +182,12 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { getMainActivity().getOverlayManager().getSideFragmentManager().show( new DisplayModeFragment()); break; - case TvOptionsManager.OPTION_PIP: + case TvOptionsManager.OPTION_IN_APP_PIP: getMainActivity().togglePipView(); break; + case TvOptionsManager.OPTION_SYSTEMWIDE_PIP: + getMainActivity().enterPictureInPictureMode(); + break; case TvOptionsManager.OPTION_MULTI_AUDIO: getMainActivity().getOverlayManager().getSideFragmentManager().show( new MultiAudioFragment()); diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java index 3ae80597..0685d14b 100644 --- a/src/com/android/tv/onboarding/OnboardingActivity.java +++ b/src/com/android/tv/onboarding/OnboardingActivity.java @@ -73,48 +73,55 @@ public class OnboardingActivity extends SetupActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Make the channels of the new inputs which have been setup outside Live TV - // browsable. - mChannelDataManager = TvApplication.getSingletons(this).getChannelDataManager(); - if (mChannelDataManager.isDbLoadFinished()) { - SetupUtils.getInstance(this).markNewChannelsBrowsable(); - } else { - mChannelDataManager.addListener(mChannelListener); + if (!PermissionUtils.hasAccessAllEpg(this)) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + Toast.makeText(this, R.string.msg_not_supported_device, Toast.LENGTH_LONG).show(); + finish(); + return; + } else if (checkSelfPermission(PERMISSION_READ_TV_LISTINGS) + != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{PERMISSION_READ_TV_LISTINGS}, + PERMISSIONS_REQUEST_READ_TV_LISTINGS); + } } } @Override protected void onDestroy() { - mChannelDataManager.removeListener(mChannelListener); + if (mChannelDataManager != null) { + mChannelDataManager.removeListener(mChannelListener); + } super.onDestroy(); } @Override protected Fragment onCreateInitialFragment() { - return OnboardingUtils.isFirstRunWithCurrentVersion(this) ? new WelcomeFragment() - : new SetupSourcesFragment(); - } - - @Override - protected void onResume() { - super.onResume(); - if (!PermissionUtils.hasAccessAllEpg(this)) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - Toast.makeText(this, R.string.msg_not_supported_device, Toast.LENGTH_LONG).show(); - finish(); - } else if (checkSelfPermission(PERMISSION_READ_TV_LISTINGS) - != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[]{PERMISSION_READ_TV_LISTINGS}, - PERMISSIONS_REQUEST_READ_TV_LISTINGS); + if (PermissionUtils.hasAccessAllEpg(this) || PermissionUtils.hasReadTvListings(this)) { + // Make the channels of the new inputs which have been setup outside Live TV + // browsable. + mChannelDataManager = TvApplication.getSingletons(this).getChannelDataManager(); + if (mChannelDataManager.isDbLoadFinished()) { + SetupUtils.getInstance(this).markNewChannelsBrowsable(); + } else { + mChannelDataManager.addListener(mChannelListener); } + return OnboardingUtils.isFirstRunWithCurrentVersion(this) ? new WelcomeFragment() + : new SetupSourcesFragment(); } + return null; } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PERMISSIONS_REQUEST_READ_TV_LISTINGS) { - if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { + if (grantResults != null && grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + finish(); + Intent intentForNextActivity = getIntent().getParcelableExtra( + KEY_INTENT_AFTER_COMPLETION); + startActivity(buildIntent(this, intentForNextActivity)); + } else { Toast.makeText(this, R.string.msg_read_tv_listing_permission_denied, Toast.LENGTH_LONG).show(); finish(); diff --git a/src/com/android/tv/onboarding/SetupSourcesFragment.java b/src/com/android/tv/onboarding/SetupSourcesFragment.java index ebf32d00..23145503 100644 --- a/src/com/android/tv/onboarding/SetupSourcesFragment.java +++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java @@ -30,7 +30,6 @@ import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.support.v17.leanback.widget.GuidedActionsStylist; import android.support.v17.leanback.widget.VerticalGridView; -import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -60,16 +59,14 @@ import java.util.List; * A fragment for channel source info/setup. */ public class SetupSourcesFragment extends SetupMultiPaneFragment { + private static final String TAG = "SetupSourcesFragment"; + public static final String ACTION_CATEGORY = "com.android.tv.onboarding.SetupSourcesFragment"; public static final int ACTION_PLAY_STORE = 1; - public static final int DEFAULT_THEME = -1; - private static final String SETUP_TRACKER_LABEL = "Setup fragment"; - private static int sTheme = DEFAULT_THEME; - private InputSetupRunnable mInputSetupRunnable; private ContentFragment mContentFragment; @@ -77,12 +74,7 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - LayoutInflater localInflater = inflater; - if (sTheme != -1) { - ContextThemeWrapper themeWrapper = new ContextThemeWrapper(getActivity(), sTheme); - localInflater = inflater.cloneInContext(themeWrapper); - } - View view = super.onCreateView(localInflater, container, savedInstanceState); + View view = super.onCreateView(inflater, container, savedInstanceState); TvApplication.getSingletons(getActivity()).getTracker().sendScreenView(SETUP_TRACKER_LABEL); return view; } @@ -97,10 +89,10 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { @Override protected SetupGuidedStepFragment onCreateContentFragment() { mContentFragment = new ContentFragment(); + mContentFragment.setParentFragment(this); Bundle arguments = new Bundle(); arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true); mContentFragment.setArguments(arguments); - mContentFragment.setParentFragment(this); return mContentFragment; } @@ -110,13 +102,6 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { } /** - * Sets the custom theme dynamically. - */ - public static void setTheme(int theme) { - sTheme = theme; - } - - /** * Call this method to run customized input setup. * * @param runnable runnable to be called when the input setup is necessary. @@ -173,6 +158,11 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { handleInputChanged(); } + @Override + public void onInputUpdated(String inputId) { + handleInputChanged(); + } + private void handleInputChanged() { // The actions created while enter transition is running will not be included in the // fragment transition. @@ -395,11 +385,6 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { updateActions(); } - @Override - public int onProvideTheme() { - return sTheme == DEFAULT_THEME ? super.onProvideTheme() : sTheme; - } - void executePendingAction() { switch (mPendingAction) { case PENDING_ACTION_INPUT_CHANGED: diff --git a/src/com/android/tv/onboarding/WelcomeFragment.java b/src/com/android/tv/onboarding/WelcomeFragment.java index ed85df68..00f7fe8d 100644 --- a/src/com/android/tv/onboarding/WelcomeFragment.java +++ b/src/com/android/tv/onboarding/WelcomeFragment.java @@ -20,8 +20,11 @@ import android.animation.Animator; import android.animation.AnimatorInflater; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; +import android.app.Activity; +import android.content.Context; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.v17.leanback.app.OnboardingFragment; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; @@ -31,7 +34,6 @@ import android.widget.ImageView; import com.android.tv.R; import com.android.tv.common.ui.setup.SetupActionHelper; import com.android.tv.common.ui.setup.animation.SetupAnimationHelper; -import com.android.tv.common.ui.setup.leanback.OnboardingFragment; import java.util.ArrayList; import java.util.List; @@ -580,7 +582,6 @@ public class WelcomeFragment extends OnboardingFragment { private ImageView mArrowView; private Animator mAnimator; - private boolean mNeedToEndAnimator; public WelcomeFragment() { setExitTransition(new SetupAnimationHelper.TransitionBuilder() @@ -589,16 +590,63 @@ public class WelcomeFragment extends OnboardingFragment { .build()); } + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + initialize(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + initialize(); + } + + private void initialize() { + if (mPageTitles == null) { + mPageTitles = getResources().getStringArray(R.array.welcome_page_titles); + mPageDescriptions = getResources().getStringArray(R.array.welcome_page_descriptions); + } + } + @Nullable @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - mPageTitles = getResources().getStringArray(R.array.welcome_page_titles); - mPageDescriptions = getResources().getStringArray(R.array.welcome_page_descriptions); - return super.onCreateView(inflater, container, savedInstanceState); + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + setLogoResourceId(R.drawable.splash_logo); + if (savedInstanceState != null) { + switch (getCurrentPageIndex()) { + case 0: + mTvContentView.setImageResource( + TV_FRAMES_1_START[TV_FRAMES_1_START.length - 1]); + break; + case 1: + mTvContentView.setImageResource( + TV_FRAMES_2_START[TV_FRAMES_2_START.length - 1]); + break; + case 2: + mTvContentView.setImageResource( + TV_FRAMES_3_ORANGE_START[TV_FRAMES_3_ORANGE_START.length - 1]); + mArrowView.setImageResource(TV_FRAMES_3_BLUE_ARROW[0]); + break; + case 3: + default: + mTvContentView.setImageResource( + TV_FRAMES_4_START[TV_FRAMES_4_START.length - 1]); + break; + } + } + return view; + } + + @Override + public int onProvideTheme() { + return R.style.Theme_Leanback_Onboarding; } @Override - protected void onStartEnterAnimation() { + protected Animator onCreateEnterAnimation() { List<Animator> animators = new ArrayList<>(); // Cloud 1 View view = getActivity().findViewById(R.id.cloud1); @@ -640,9 +688,7 @@ public class WelcomeFragment extends OnboardingFragment { animators.add(animator); AnimatorSet set = new AnimatorSet(); set.playTogether(animators); - mAnimator = set; - mAnimator.start(); - mNeedToEndAnimator = true; + return set; } @Nullable @@ -683,23 +729,14 @@ public class WelcomeFragment extends OnboardingFragment { } @Override - protected int getLogoResourceId() { - return R.drawable.splash_logo; - } - - @Override protected void onFinishFragment() { SetupActionHelper.onActionClick(WelcomeFragment.this, ACTION_CATEGORY, ACTION_NEXT); } @Override - protected void onStartPageChangeAnimation(int previousPage) { + protected void onPageChanged(int newPage, int previousPage) { if (mAnimator != null) { - if (mNeedToEndAnimator) { - mAnimator.end(); - } else { - mAnimator.cancel(); - } + mAnimator.cancel(); } mArrowView.setVisibility(View.GONE); // TV screen hiding animator. @@ -710,7 +747,7 @@ public class WelcomeFragment extends OnboardingFragment { // TV screen showing animator. AnimatorSet animatorSet = new AnimatorSet(); int firstFrame; - switch (getCurrentPageIndex()) { + switch (newPage) { case 0: animatorSet.playSequentially(hideAnimator, SetupAnimationHelper.createFrameAnimator(mTvContentView, @@ -762,6 +799,5 @@ public class WelcomeFragment extends OnboardingFragment { }); mAnimator = SetupAnimationHelper.applyAnimationTimeScale(animatorSet); mAnimator.start(); - mNeedToEndAnimator = false; } } diff --git a/src/com/android/tv/parental/ContentRatingSystem.java b/src/com/android/tv/parental/ContentRatingSystem.java index 6c00ee11..6b5d6635 100644 --- a/src/com/android/tv/parental/ContentRatingSystem.java +++ b/src/com/android/tv/parental/ContentRatingSystem.java @@ -490,6 +490,19 @@ public class ContentRatingSystem { mRatingOrder = ratingOrder; } + /** + * Returns index of the rating in this order. + * Returns -1 if this order doesn't contain the rating. + */ + public int getRatingIndex(Rating rating) { + for (int i = 0; i < mRatingOrder.size(); i++) { + if (mRatingOrder.get(i).getName().equals(rating.getName())) { + return i; + } + } + return -1; + } + public static class Builder { private final List<String> mRatingNames = new ArrayList<>(); diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java index 3cd6186c..da88f70d 100644 --- a/src/com/android/tv/receiver/BootCompletedReceiver.java +++ b/src/com/android/tv/receiver/BootCompletedReceiver.java @@ -21,6 +21,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.support.v4.os.BuildCompat; import android.util.Log; import com.android.tv.Features; @@ -72,8 +73,7 @@ public class BootCompletedReceiver extends BroadcastReceiver { } } - // DVR - if (CommonFeatures.DVR.isEnabled(context)) { + if (CommonFeatures.DVR.isEnabled(context) && BuildCompat.isAtLeastN()) { DvrRecordingService.startService(context); } } diff --git a/src/com/android/tv/receiver/GlobalKeyReceiver.java b/src/com/android/tv/receiver/GlobalKeyReceiver.java index bd81cee3..2e19c089 100644 --- a/src/com/android/tv/receiver/GlobalKeyReceiver.java +++ b/src/com/android/tv/receiver/GlobalKeyReceiver.java @@ -19,6 +19,7 @@ package com.android.tv.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.media.tv.TvContract; import android.util.Log; import android.view.KeyEvent; @@ -39,10 +40,22 @@ public class GlobalKeyReceiver extends BroadcastReceiver { if (DEBUG) Log.d(TAG, "onReceive: " + event); int keyCode = event.getKeyCode(); int action = event.getAction(); - if (keyCode == KeyEvent.KEYCODE_TV && action == KeyEvent.ACTION_UP) { - ((TvApplication) context.getApplicationContext()).handleTvKey(); - } else if (keyCode == KeyEvent.KEYCODE_TV_INPUT && action == KeyEvent.ACTION_UP) { - ((TvApplication) context.getApplicationContext()).handleTvInputKey(); + if (action == KeyEvent.ACTION_UP) { + switch (keyCode) { + case KeyEvent.KEYCODE_GUIDE: + context.startActivity( + new Intent(Intent.ACTION_VIEW, TvContract.Programs.CONTENT_URI)); + break; + case KeyEvent.KEYCODE_TV: + ((TvApplication) context.getApplicationContext()).handleTvKey(); + break; + case KeyEvent.KEYCODE_TV_INPUT: + ((TvApplication) context.getApplicationContext()).handleTvInputKey(); + break; + default: + // Do nothing + break; + } } } } diff --git a/src/com/android/tv/receiver/PackageIntentsReceiver.java b/src/com/android/tv/receiver/PackageIntentsReceiver.java index 67f0529f..4c850402 100644 --- a/src/com/android/tv/receiver/PackageIntentsReceiver.java +++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java @@ -17,59 +17,18 @@ package com.android.tv.receiver; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import com.android.tv.TvActivity; import com.android.tv.TvApplication; -import com.android.usbtuner.setup.TunerSetupActivity; -import com.android.usbtuner.UsbTunerPreferences; -import com.android.usbtuner.tvinput.UsbTunerTvInputService; /** * A class for handling the broadcast intents from PackageManager. */ public class PackageIntentsReceiver extends BroadcastReceiver { - private PackageManager mPackageManager; - private ComponentName mTvActivityComponentName; - private ComponentName mUsbTunerComponentName; - - private void init(Context context) { - mPackageManager = context.getPackageManager(); - mTvActivityComponentName = new ComponentName(context, TvActivity.class); - mUsbTunerComponentName = new ComponentName(context, UsbTunerTvInputService.class); - } @Override public void onReceive(Context context, Intent intent) { - if (mPackageManager == null) { - init(context); - } ((TvApplication) context.getApplicationContext()).handleInputCountChanged(); - // Check the component status of UsbTunerTvInputService and TvActivity here to make sure - // start the setup activity of USB tuner TV input service only when those components are - // enabled. - if (UsbTunerPreferences.shouldShowSetupActivity(context) - && Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction()) - && mPackageManager.getComponentEnabledSetting(mTvActivityComponentName) - == PackageManager.COMPONENT_ENABLED_STATE_ENABLED - && mPackageManager.getComponentEnabledSetting(mUsbTunerComponentName) - == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { - startUsbTunerSetupActivity(context); - UsbTunerPreferences.setShouldShowSetupActivity(context, false); - } - } - - /** - * Launches the setup activity of USB tuner TV input service. - * - * @param context {@link Context} instance - */ - private static void startUsbTunerSetupActivity(Context context) { - Intent intent = TunerSetupActivity.createSetupActivity(context); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); } } diff --git a/src/com/android/tv/recommendation/NotificationService.java b/src/com/android/tv/recommendation/NotificationService.java index c6a0c3f6..0095482d 100644 --- a/src/com/android/tv/recommendation/NotificationService.java +++ b/src/com/android/tv/recommendation/NotificationService.java @@ -28,6 +28,7 @@ import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Rect; import android.media.tv.TvInputInfo; +import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; @@ -51,6 +52,7 @@ import com.android.tv.data.Program; import com.android.tv.util.BitmapUtils; import com.android.tv.util.BitmapUtils.ScaledBitmapInfo; import com.android.tv.util.ImageLoader; +import com.android.tv.util.PermissionUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -127,6 +129,12 @@ public class NotificationService extends Service implements Recommender.Listener public void onCreate() { if (DEBUG) Log.d(TAG, "onCreate"); super.onCreate(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M + && !PermissionUtils.hasAccessAllEpg(this)) { + Log.w(TAG, "Live TV requires the system permission on this platform."); + stopSelf(); + return; + } mCurrentNotificationCount = 0; mNotificationChannels = new long[NOTIFICATION_COUNT]; diff --git a/src/com/android/tv/recommendation/RecommendationDataManager.java b/src/com/android/tv/recommendation/RecommendationDataManager.java index 66dd9fe4..a7d4c46d 100644 --- a/src/com/android/tv/recommendation/RecommendationDataManager.java +++ b/src/com/android/tv/recommendation/RecommendationDataManager.java @@ -16,7 +16,6 @@ package com.android.tv.recommendation; -import android.content.ContentUris; import android.content.Context; import android.content.UriMatcher; import android.database.ContentObserver; @@ -35,8 +34,10 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; +import com.android.tv.TvApplication; import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.data.WatchedHistoryManager; import com.android.tv.util.PermissionUtils; @@ -51,8 +52,6 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class RecommendationDataManager implements WatchedHistoryManager.Listener { - private static final String TAG = "RecommendationDataManager"; - private static final UriMatcher sUriMatcher; private static final int MATCH_CHANNEL = 1; private static final int MATCH_CHANNEL_ID = 2; @@ -66,19 +65,15 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener private static final int MSG_START = 1000; private static final int MSG_STOP = 1001; - private static final int MSG_UPDATE_CHANNEL = 1002; - private static final int MSG_UPDATE_CHANNELS = 1003; - private static final int MSG_UPDATE_WATCH_HISTORY = 1004; - private static final int MSG_NOTIFY_CHANNEL_RECORD_MAP_LOADED = 1005; - private static final int MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED = 1006; + private static final int MSG_UPDATE_CHANNELS = 1002; + private static final int MSG_UPDATE_WATCH_HISTORY = 1003; + private static final int MSG_NOTIFY_CHANNEL_RECORD_MAP_LOADED = 1004; + private static final int MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED = 1005; private static final int MSG_FIRST = MSG_START; private static final int MSG_LAST = MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED; - private static final int INVALID_INDEX = -1; - private static RecommendationDataManager sManager; - private final static Object sListenerLock = new Object(); private final ContentObserver mContentObserver; private final Map<Long, ChannelRecord> mChannelRecordMap = new ConcurrentHashMap<>(); private final Map<Long, ChannelRecord> mAvailableChannelRecordMap = new ConcurrentHashMap<>(); @@ -98,10 +93,33 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener private final HandlerThread mHandlerThread; private final Handler mHandler; + private final Handler mMainHandler; @Nullable private WatchedHistoryManager mWatchedHistoryManager; + private final ChannelDataManager mChannelDataManager; + private final ChannelDataManager.Listener mChannelDataListener = + new ChannelDataManager.Listener() { + @Override + @MainThread + public void onLoadFinished() { + updateChannelData(); + } - private final List<ListenerRecord> mListeners = new ArrayList<>(); + @Override + @MainThread + public void onChannelListUpdated() { + updateChannelData(); + } + + @Override + @MainThread + public void onChannelBrowsableChanged() { + updateChannelData(); + } + }; + + // For thread safety, this variable is handled only on main thread. + private final List<Listener> mListeners = new ArrayList<>(); /** * Gets instance of RecommendationDataManager, and adds a {@link Listener}. @@ -112,25 +130,11 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener public synchronized static RecommendationDataManager acquireManager( Context context, @NonNull Listener listener) { if (sManager == null) { - sManager = new RecommendationDataManager(context); + sManager = new RecommendationDataManager(context, listener); } - sManager.addListener(listener); - sManager.start(); return sManager; } - /** - * Removes the {@link Listener}, and releases RecommendationDataManager - * if there are no listeners remained. - */ - public void release(@NonNull Listener listener) { - removeListener(listener); - synchronized (sListenerLock) { - if (mListeners.size() == 0) { - stop(); - } - } - } private final TvInputCallback mInternalCallback = new TvInputCallback() { @Override @@ -187,12 +191,37 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener public void onInputUpdated(String inputId) { } }; - private RecommendationDataManager(Context context) { + private RecommendationDataManager(Context context, final Listener listener) { mContext = context.getApplicationContext(); mHandlerThread = new HandlerThread("RecommendationDataManager"); mHandlerThread.start(); mHandler = new RecommendationHandler(mHandlerThread.getLooper(), this); + mMainHandler = new RecommendationMainHandler(Looper.getMainLooper(), this); mContentObserver = new RecommendationContentObserver(mHandler); + mChannelDataManager = TvApplication.getSingletons(mContext).getChannelDataManager(); + runOnMainThread(new Runnable() { + @Override + public void run() { + addListener(listener); + start(); + } + }); + } + + /** + * Removes the {@link Listener}, and releases RecommendationDataManager + * if there are no listeners remained. + */ + public void release(@NonNull final Listener listener) { + runOnMainThread(new Runnable() { + @Override + public void run() { + removeListener(listener); + if (mListeners.size() == 0) { + stop(); + } + } + }); } /** @@ -216,54 +245,48 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener return Collections.unmodifiableCollection(mAvailableChannelRecordMap.values()); } + @MainThread private void start() { mHandler.sendEmptyMessage(MSG_START); + mChannelDataManager.addListener(mChannelDataListener); + if (mChannelDataManager.isDbLoadFinished()) { + updateChannelData(); + } } + @MainThread private void stop() { for (int what = MSG_FIRST; what <= MSG_LAST; ++what) { mHandler.removeMessages(what); } + mChannelDataManager.removeListener(mChannelDataListener); mHandler.sendEmptyMessage(MSG_STOP); mHandlerThread.quitSafely(); + mMainHandler.removeCallbacksAndMessages(null); sManager = null; } - private int getListenerIndexLocked(Listener listener) { - for (int i = 0; i < mListeners.size(); ++i) { - if (mListeners.get(i).mListener == listener) { - return i; - } - } - return INVALID_INDEX; + @MainThread + private void updateChannelData() { + mHandler.removeMessages(MSG_UPDATE_CHANNELS); + mHandler.obtainMessage(MSG_UPDATE_CHANNELS, mChannelDataManager.getBrowsableChannelList()) + .sendToTarget(); } + @MainThread private void addListener(Listener listener) { - synchronized (sListenerLock) { - if (getListenerIndexLocked(listener) == INVALID_INDEX) { - mListeners.add((new ListenerRecord(listener))); - } - } + mListeners.add(listener); } + @MainThread private void removeListener(Listener listener) { - synchronized (sListenerLock) { - int idx = getListenerIndexLocked(listener); - if (idx != INVALID_INDEX) { - ListenerRecord record = mListeners.remove(idx); - record.mListener = null; - } - } + mListeners.remove(listener); } private void onStart() { if (!mStarted) { mStarted = true; mCancelLoadTask = false; - mContext.getContentResolver().registerContentObserver( - TvContract.Channels.CONTENT_URI, true, mContentObserver); - mHandler.obtainMessage(MSG_UPDATE_CHANNELS, TvContract.Channels.CONTENT_URI) - .sendToTarget(); if (!PermissionUtils.hasAccessWatchedHistory(mContext)) { mWatchedHistoryManager = new WatchedHistoryManager(mContext); mWatchedHistoryManager.setListener(this); @@ -297,42 +320,7 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener } @WorkerThread - private void onUpdateChannel(Uri uri) { - Channel channel = null; - try (Cursor cursor = mContext.getContentResolver().query(uri, Channel.PROJECTION, - null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - channel = Channel.fromCursor(cursor); - } - } - boolean isChannelRecordMapChanged = false; - if (channel == null) { - long channelId = ContentUris.parseId(uri); - mChannelRecordMap.remove(channelId); - isChannelRecordMapChanged = mAvailableChannelRecordMap.remove(channelId) != null; - } else if (updateChannelRecordMapFromChannel(channel)) { - isChannelRecordMapChanged = true; - } - if (isChannelRecordMapChanged && mChannelRecordMapLoaded - && !mHandler.hasMessages(MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED)) { - mHandler.sendEmptyMessage(MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED); - } - } - - @WorkerThread - private void onUpdateChannels(Uri uri) { - List<Channel> channels = new ArrayList<>(); - try (Cursor cursor = mContext.getContentResolver().query(uri, Channel.PROJECTION, - null, null, null)) { - if (cursor != null) { - while (cursor.moveToNext()) { - if (mCancelLoadTask) { - return; - } - channels.add(Channel.fromCursor(cursor)); - } - } - } + private void onUpdateChannels(List<Channel> channels) { boolean isChannelRecordMapChanged = false; Set<Long> removedChannelIdSet = new HashSet<>(mChannelRecordMap.keySet()); // Builds removedChannelIdSet. @@ -374,11 +362,14 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener final ChannelRecord channelRecord = updateChannelRecordFromWatchedProgram(watchedProgram); if (mChannelRecordMapLoaded && channelRecord != null) { - synchronized (sListenerLock) { - for (ListenerRecord l : mListeners) { - l.postNewWatchLog(channelRecord); + runOnMainThread(new Runnable() { + @Override + public void run() { + for (Listener l : mListeners) { + l.onNewWatchLog(channelRecord); + } } - } + }); } } if (!mChannelRecordMapLoaded) { @@ -410,14 +401,17 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener @Override public void onNewRecordAdded(WatchedHistoryManager.WatchedRecord watchedRecord) { - ChannelRecord channelRecord = updateChannelRecordFromWatchedProgram( + final ChannelRecord channelRecord = updateChannelRecordFromWatchedProgram( convertFromWatchedHistoryManagerRecords(watchedRecord)); if (mChannelRecordMapLoaded && channelRecord != null) { - synchronized (sListenerLock) { - for (ListenerRecord l : mListeners) { - l.postNewWatchLog(channelRecord); + runOnMainThread(new Runnable() { + @Override + public void run() { + for (Listener l : mListeners) { + l.onNewWatchLog(channelRecord); + } } - } + }); } } @@ -452,19 +446,25 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener private void onNotifyChannelRecordMapLoaded() { mChannelRecordMapLoaded = true; - synchronized (sListenerLock) { - for (ListenerRecord l : mListeners) { - l.postChannelRecordLoaded(); + runOnMainThread(new Runnable() { + @Override + public void run() { + for (Listener l : mListeners) { + l.onChannelRecordLoaded(); + } } - } + }); } private void onNotifyChannelRecordMapChanged() { - synchronized (sListenerLock) { - for (ListenerRecord l : mListeners) { - l.postChannelRecordChanged(); + runOnMainThread(new Runnable() { + @Override + public void run() { + for (Listener l : mListeners) { + l.onChannelRecordChanged(); + } } - } + }); } /** @@ -511,15 +511,6 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener @Override public void onChange(final boolean selfChange, final Uri uri) { switch (sUriMatcher.match(uri)) { - case MATCH_CHANNEL: - if (!mHandler.hasMessages(MSG_UPDATE_CHANNELS, TvContract.Channels.CONTENT_URI)) { - mHandler.obtainMessage(MSG_UPDATE_CHANNELS, TvContract.Channels.CONTENT_URI) - .sendToTarget(); - } - break; - case MATCH_CHANNEL_ID: - mHandler.obtainMessage(MSG_UPDATE_CHANNEL, uri).sendToTarget(); - break; case MATCH_WATCHED_PROGRAM_ID: if (!mHandler.hasMessages(MSG_UPDATE_WATCH_HISTORY, TvContract.WatchedPrograms.CONTENT_URI)) { @@ -530,6 +521,14 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener } } + private void runOnMainThread(Runnable r) { + if (Looper.myLooper() == Looper.getMainLooper()) { + r.run(); + } else { + mMainHandler.post(r); + } + } + /** * A listener interface to receive notification about the recommendation data. * @@ -561,55 +560,6 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener void onChannelRecordChanged(); } - private static class ListenerRecord { - private Listener mListener; - private final Handler mHandler; - - public ListenerRecord(Listener listener) { - mHandler = new Handler(); - mListener = listener; - } - - public void postChannelRecordLoaded() { - mHandler.post(new Runnable() { - @Override - public void run() { - synchronized (sListenerLock) { - if (mListener != null) { - mListener.onChannelRecordLoaded(); - } - } - } - }); - } - - public void postNewWatchLog(final ChannelRecord channelRecord) { - mHandler.post(new Runnable() { - @Override - public void run() { - synchronized (sListenerLock) { - if (mListener != null) { - mListener.onNewWatchLog(channelRecord); - } - } - } - }); - } - - public void postChannelRecordChanged() { - mHandler.post(new Runnable() { - @Override - public void run() { - synchronized (sListenerLock) { - if (mListener != null) { - mListener.onChannelRecordChanged(); - } - } - } - }); - } - } - private static class RecommendationHandler extends WeakHandler<RecommendationDataManager> { public RecommendationHandler(@NonNull Looper looper, RecommendationDataManager ref) { super(looper, ref); @@ -626,14 +576,9 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener dataManager.onStop(); } break; - case MSG_UPDATE_CHANNEL: - if (dataManager.mStarted) { - dataManager.onUpdateChannel((Uri) msg.obj); - } - break; case MSG_UPDATE_CHANNELS: if (dataManager.mStarted) { - dataManager.onUpdateChannels((Uri) msg.obj); + dataManager.onUpdateChannels((List<Channel>) msg.obj); } break; case MSG_UPDATE_WATCH_HISTORY: @@ -654,4 +599,13 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener } } } + + private static class RecommendationMainHandler extends WeakHandler<RecommendationDataManager> { + public RecommendationMainHandler(@NonNull Looper looper, RecommendationDataManager ref) { + super(looper, ref); + } + + @Override + protected void handleMessage(Message msg, @NonNull RecommendationDataManager referent) { } + } } diff --git a/src/com/android/tv/recommendation/Recommender.java b/src/com/android/tv/recommendation/Recommender.java index 0561449e..82c2893d 100644 --- a/src/com/android/tv/recommendation/Recommender.java +++ b/src/com/android/tv/recommendation/Recommender.java @@ -145,7 +145,7 @@ public class Recommender implements RecommendationDataManager.Listener { mChannelSortKey.put(records.get(i).first.getId(), String.format(sortKeyFormat, i)); results.add(records.get(i).first); } - return Collections.unmodifiableList(results); + return results; } /** diff --git a/src/com/android/tv/recommendation/RoutineWatchEvaluator.java b/src/com/android/tv/recommendation/RoutineWatchEvaluator.java index 694da6bf..5ff7cae9 100644 --- a/src/com/android/tv/recommendation/RoutineWatchEvaluator.java +++ b/src/com/android/tv/recommendation/RoutineWatchEvaluator.java @@ -16,7 +16,9 @@ package com.android.tv.recommendation; +import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; import com.android.tv.data.Program; @@ -36,7 +38,6 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator { private static final double TIME_MATCH_WEIGHT = 1 - TITLE_MATCH_WEIGHT; private static final long DIFF_MS_TOLERANCE_FOR_OLD_PROGRAM = TimeUnit.DAYS.toMillis(14); private static final long MAX_DIFF_MS_FOR_OLD_PROGRAM = TimeUnit.DAYS.toMillis(56); - private static final String REGULAR_EXPRESSION_FOR_WHITE_SPACES = "\\s+"; @Override public double evaluateChannel(long channelId) { @@ -91,8 +92,8 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator { return maxScore; } - private double calculateRoutineWatchScore( - Program currentProgram, Program watchedProgram, long watchedDurationMs) { + private static double calculateRoutineWatchScore(Program currentProgram, Program watchedProgram, + long watchedDurationMs) { double timeMatchScore = calculateTimeMatchScore(currentProgram, watchedProgram); double titleMatchScore = calculateTitleMatchScore( currentProgram.getTitle(), watchedProgram.getTitle()); @@ -107,10 +108,16 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator { * watchDurationScore * multiplierForOldProgram; } - private double calculateTitleMatchScore(String title1, String title2) { + @VisibleForTesting + static double calculateTitleMatchScore(@Nullable String title1, @Nullable String title2) { + if (TextUtils.isEmpty(title1) || TextUtils.isEmpty(title2)) { + return 0; + } List<String> wordList1 = splitTextToWords(title1); List<String> wordList2 = splitTextToWords(title2); - + if (wordList1.isEmpty() || wordList2.isEmpty()) { + return 0; + } int maxMatchedWordSeqLen = calculateMaximumMatchedWordSequenceLength( wordList1, wordList2); @@ -121,8 +128,8 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator { } @VisibleForTesting - int calculateMaximumMatchedWordSequenceLength( - List<String> toSearchWords, List<String> toMatchWords) { + static int calculateMaximumMatchedWordSequenceLength(List<String> toSearchWords, + List<String> toMatchWords) { int[] matchedWordSeqLen = new int[toMatchWords.size()]; int maxMatchedWordSeqLen = 0; for (String word : toSearchWords) { @@ -142,7 +149,7 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator { return maxMatchedWordSeqLen; } - private double calculateTimeMatchScore(Program p1, Program p2) { + private static double calculateTimeMatchScore(Program p1, Program p2) { ProgramTime t1 = ProgramTime.createFromProgram(p1); ProgramTime t2 = ProgramTime.createFromProgram(p2); @@ -155,7 +162,7 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator { } @VisibleForTesting - double calculateOverlappedIntervalScore(ProgramTime t1, ProgramTime t2) { + static double calculateOverlappedIntervalScore(ProgramTime t1, ProgramTime t2) { if (t1.dayChanged && !t2.dayChanged) { // Swap two values. return calculateOverlappedIntervalScore(t2, t1); @@ -181,7 +188,7 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator { return score; } - private double calculateWatchDurationScore(Program program, long durationMs) { + private static double calculateWatchDurationScore(Program program, long durationMs) { return (double) durationMs / (program.getEndTimeUtcMillis() - program.getStartTimeUtcMillis()); } diff --git a/src/com/android/tv/ui/AppLayerTvView.java b/src/com/android/tv/ui/AppLayerTvView.java index befa004c..c7b94a15 100644 --- a/src/com/android/tv/ui/AppLayerTvView.java +++ b/src/com/android/tv/ui/AppLayerTvView.java @@ -16,9 +16,8 @@ package com.android.tv.ui; -import com.android.tv.common.recording.PlaybackTvView; - import android.content.Context; +import android.media.tv.TvView; import android.util.AttributeSet; /** @@ -30,7 +29,7 @@ import android.util.AttributeSet; * TODO: remove this class once the TvView.setMain() is revisited. * </p> */ -public class AppLayerTvView extends PlaybackTvView { +public class AppLayerTvView extends TvView { public AppLayerTvView(Context context) { super(context); } diff --git a/src/com/android/tv/ui/BlockScreenView.java b/src/com/android/tv/ui/BlockScreenView.java new file mode 100644 index 00000000..52b9389d --- /dev/null +++ b/src/com/android/tv/ui/BlockScreenView.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.ui; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorInflater; +import android.animation.AnimatorListenerAdapter; +import android.content.Context; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.tv.R; +import com.android.tv.ui.TunableTvView.BlockScreenType; + +public class BlockScreenView extends LinearLayout { + private View mContainerView; + private View mImageContainer; + private ImageView mNormalImageView; + private ImageView mShrunkenImageView; + private View mSpace; + private TextView mTextView; + + private final int mSpacingNormal; + private final int mSpacingShrunken; + + // Animators used for fade in/out of block screen icon. + private Animator mFadeIn; + private Animator mFadeOut; + + public BlockScreenView(Context context) { + this(context, null, 0); + } + + public BlockScreenView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BlockScreenView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mSpacingNormal = getResources().getDimensionPixelOffset( + R.dimen.tvview_block_vertical_spacing); + mSpacingShrunken = getResources().getDimensionPixelOffset( + R.dimen.shrunken_tvview_block_vertical_spacing); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mContainerView = findViewById(R.id.block_screen_container); + mImageContainer = findViewById(R.id.image_container); + mNormalImageView = (ImageView) findViewById(R.id.block_screen_icon); + mShrunkenImageView = (ImageView) findViewById(R.id.block_screen_shrunken_icon); + mSpace = findViewById(R.id.space); + mTextView = (TextView) findViewById(R.id.block_screen_text); + mFadeIn = AnimatorInflater.loadAnimator(getContext(), + R.animator.tvview_block_screen_fade_in); + mFadeIn.setTarget(mContainerView); + mFadeOut = AnimatorInflater.loadAnimator(getContext(), + R.animator.tvview_block_screen_fade_out); + mFadeOut.setTarget(mContainerView); + mFadeOut.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mContainerView.setVisibility(GONE); + mContainerView.setAlpha(1f); + } + }); + } + + /** + * Sets the normal image. + */ + public void setImage(int resId) { + mNormalImageView.setImageResource(resId); + updateSpaceVisibility(); + } + + /** + * Sets the scale type of the normal image. + */ + public void setScaleType(ScaleType scaleType) { + mNormalImageView.setScaleType(scaleType); + updateSpaceVisibility(); + } + + /** + * Sets the shrunken image. + */ + public void setShrunkenImage(int resId) { + mShrunkenImageView.setImageResource(resId); + updateSpaceVisibility(); + } + + /** + * Show or hide the image of this view. + */ + public void setImageVisibility(boolean visible) { + mImageContainer.setVisibility(visible ? VISIBLE : GONE); + updateSpaceVisibility(); + } + + /** + * Sets the text message. + */ + public void setText(int resId) { + mTextView.setText(resId); + updateSpaceVisibility(); + } + + /** + * Sets the text message. + */ + public void setText(String text) { + mTextView.setText(text); + updateSpaceVisibility(); + } + + private void updateSpaceVisibility() { + if (isImageViewVisible() && isTextViewVisible(mTextView)) { + mSpace.setVisibility(VISIBLE); + } else { + mSpace.setVisibility(GONE); + } + } + + private boolean isImageViewVisible() { + return mImageContainer.getVisibility() == VISIBLE + && (isImageViewVisible(mNormalImageView) || isImageViewVisible(mShrunkenImageView)); + } + + private static boolean isImageViewVisible(ImageView imageView) { + return imageView.getVisibility() != GONE && imageView.getDrawable() != null; + } + + private static boolean isTextViewVisible(TextView textView) { + return textView.getVisibility() != GONE && !TextUtils.isEmpty(textView.getText()); + } + + /** + * Changes the spacing between the image view and the text view according to the + * {@code blockScreenType}. + */ + public void setSpacing(@BlockScreenType int blockScreenType) { + mSpace.getLayoutParams().height = + blockScreenType == TunableTvView.BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW + ? mSpacingShrunken : mSpacingNormal; + requestLayout(); + } + + /** + * Changes the view layout according to the {@code blockScreenType}. + */ + public void onBlockStatusChanged(@BlockScreenType int blockScreenType, boolean withAnimation) { + if (!withAnimation) { + switch (blockScreenType) { + case TunableTvView.BLOCK_SCREEN_TYPE_NO_UI: + mContainerView.setVisibility(GONE); + break; + case TunableTvView.BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW: + mNormalImageView.setVisibility(GONE); + mShrunkenImageView.setVisibility(VISIBLE); + mContainerView.setVisibility(VISIBLE); + break; + case TunableTvView.BLOCK_SCREEN_TYPE_NORMAL: + mNormalImageView.setVisibility(VISIBLE); + mShrunkenImageView.setVisibility(GONE); + mContainerView.setVisibility(VISIBLE); + break; + } + } else { + switch (blockScreenType) { + case TunableTvView.BLOCK_SCREEN_TYPE_NO_UI: + if (mContainerView.getVisibility() == VISIBLE) { + mFadeOut.start(); + } + break; + case TunableTvView.BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW: + mNormalImageView.setVisibility(GONE); + mShrunkenImageView.setVisibility(VISIBLE); + mContainerView.setVisibility(VISIBLE); + if (mContainerView.getVisibility() == GONE) { + mFadeIn.start(); + } + break; + case TunableTvView.BLOCK_SCREEN_TYPE_NORMAL: + mNormalImageView.setVisibility(VISIBLE); + mShrunkenImageView.setVisibility(GONE); + mContainerView.setVisibility(VISIBLE); + if (mContainerView.getVisibility() == GONE) { + mFadeIn.start(); + } + break; + } + } + updateSpaceVisibility(); + } + + /** + * Scales the contents view by the given {@code scale}. + */ + public void scaleContainerView(float scale) { + mContainerView.setScaleX(scale); + mContainerView.setScaleY(scale); + } + + public void addFadeOutAnimationListener(AnimatorListener listener) { + mFadeOut.addListener(listener); + } + + /** + * Ends the currently running animations. + */ + public void endAnimations() { + if (mFadeIn != null && mFadeIn.isRunning()) { + mFadeIn.end(); + } + if (mFadeOut != null && mFadeOut.isRunning()) { + mFadeOut.end(); + } + } +} diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java index 17ac8f3b..a36ba83c 100644 --- a/src/com/android/tv/ui/ChannelBannerView.java +++ b/src/com/android/tv/ui/ChannelBannerView.java @@ -16,8 +16,6 @@ package com.android.tv.ui; -import static com.android.tv.util.ImageLoader.ImageLoaderCallback; - import android.animation.Animator; import android.animation.AnimatorInflater; import android.animation.AnimatorListenerAdapter; @@ -35,8 +33,10 @@ import android.support.annotation.Nullable; import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; +import android.text.format.DateUtils; import android.text.style.TextAppearanceSpan; import android.util.AttributeSet; +import android.util.Log; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; @@ -50,11 +50,13 @@ import android.widget.TextView; import com.android.tv.MainActivity; import com.android.tv.R; +import com.android.tv.common.recording.RecordedProgram; import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.data.StreamInfo; import com.android.tv.util.ImageCache; import com.android.tv.util.ImageLoader; +import com.android.tv.util.ImageLoader.ImageLoaderCallback; import com.android.tv.util.ImageLoader.LoadTvInputLogoTask; import com.android.tv.util.Utils; @@ -66,6 +68,8 @@ import java.util.Objects; * A view to render channel banner. */ public class ChannelBannerView extends FrameLayout implements TvTransitionManager.TransitionLayout { + private static final String TAG = "ChannelBannerView"; + private static final boolean DEBUG = false; /** * Show all information at the channel banner. @@ -111,6 +115,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private View mAnchorView; private Channel mCurrentChannel; private Program mLastUpdatedProgram; + private RecordedProgram mLastUpdatedRecordedProgram; private final Handler mHandler = new Handler(); private int mLockType; @@ -235,6 +240,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage @Override protected void onAttachedToWindow() { + if (DEBUG) Log.d(TAG, "onAttachedToWindow"); super.onAttachedToWindow(); getContext().getContentResolver().registerContentObserver(TvContract.Programs.CONTENT_URI, true, mProgramUpdateObserver); @@ -242,6 +248,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage @Override protected void onDetachedFromWindow() { + if (DEBUG) Log.d(TAG, "onDetachedToWindow"); getContext().getContentResolver().unregisterContentObserver(mProgramUpdateObserver); super.onDetachedFromWindow(); } @@ -329,8 +336,6 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage /** * Update channel banner view. - * Note that this only updates the channel banner contents, - * and use onBeforeShow() or onAfterHide() for showing/hiding. * * @param info A StreamInfo that includes stream information. * If it's {@code null}, only program information will be updated. @@ -342,19 +347,19 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage scheduleHide(); } mCurrentChannel = channel; - if (channel == null) { - mLastUpdatedProgram = null; - updateProgramInfo(null); - return; - } mChannelView.setVisibility(VISIBLE); if (info != null) { // If the current channels between ChannelTuner and TvView are different, // the stream information should not be seen. - updateStreamInfo(channel.equals(info.getCurrentChannel()) ? info : null); + updateStreamInfo(channel != null && channel.equals(info.getCurrentChannel()) ? info + : null); updateChannelInfo(); } - updateProgramInfo(mMainActivity.getCurrentProgram()); + if (mMainActivity.isRecordingPlayback()) { + updateProgramInfo(mMainActivity.getPlayingRecordedProgram()); + } else { + updateProgramInfo(mMainActivity.getCurrentProgram()); + } } private void updateStreamInfo(StreamInfo info) { @@ -363,7 +368,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage updateText(mClosedCaptionTextView, info.hasClosedCaption() ? sClosedCaptionMark : EMPTY_STRING); updateText(mAspectRatioTextView, - Utils.getAspectRatioString(info.getVideoWidth(), info.getVideoHeight())); + Utils.getAspectRatioString(info.getVideoDisplayAspectRatio())); updateText(mResolutionTextView, Utils.getVideoDefinitionLevelString( mMainActivity, info.getVideoDefinitionLevel())); @@ -380,11 +385,24 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private void updateChannelInfo() { // Update static information for a channel. - String displayNumber = mCurrentChannel.getDisplayNumber(); - if (displayNumber == null) { - displayNumber = EMPTY_STRING; + String displayNumber = EMPTY_STRING; + String displayName = EMPTY_STRING; + if (mCurrentChannel != null) { + displayNumber = mCurrentChannel.getDisplayNumber(); + if (displayNumber == null) { + displayNumber = EMPTY_STRING; + } + displayName = mCurrentChannel.getDisplayName(); + if (displayName == null) { + displayName = EMPTY_STRING; + } } + if (displayNumber.isEmpty()) { + mChannelNumberTextView.setVisibility(GONE); + } else { + mChannelNumberTextView.setVisibility(VISIBLE); + } if (displayNumber.length() <= 3) { updateTextView( mChannelNumberTextView, @@ -402,14 +420,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage R.dimen.channel_banner_channel_number_small_margin_top); } mChannelNumberTextView.setText(displayNumber); - String displayName = mCurrentChannel.getDisplayName(); - if (displayName == null) { - displayName = EMPTY_STRING; - } mChannelNameTextView.setText(displayName); - TvInputInfo info = mMainActivity.getTvInputManagerHelper().getTvInputInfo( - mCurrentChannel.getInputId()); + getCurrentInputId()); if (info == null || !ImageLoader.loadBitmap(createTvInputLogoLoaderCallback(info, this), new LoadTvInputLogoTask(getContext(), ImageCache.getInstance(), info))) { mTvInputLogoImageView.setVisibility(View.GONE); @@ -417,9 +430,24 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage } mChannelLogoImageView.setImageBitmap(null); mChannelLogoImageView.setVisibility(View.GONE); - mCurrentChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, - mChannelLogoImageViewWidth, mChannelLogoImageViewHeight, - createChannelLogoCallback(this, mCurrentChannel)); + if (mCurrentChannel != null) { + mCurrentChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, + mChannelLogoImageViewWidth, mChannelLogoImageViewHeight, + createChannelLogoCallback(this, mCurrentChannel)); + } + } + + private String getCurrentInputId() { + Channel channel = mMainActivity.getCurrentChannel(); + if (channel != null) { + return channel.getInputId(); + } else if (mMainActivity.isRecordingPlayback()) { + RecordedProgram recordedProgram = mMainActivity.getPlayingRecordedProgram(); + if (recordedProgram != null) { + return recordedProgram.getInputId(); + } + } + return null; } private void updateTvInputLogo(Bitmap bitmap) { @@ -432,8 +460,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage return new ImageLoaderCallback<ChannelBannerView>(channelBannerView) { @Override public void onBitmapLoaded(ChannelBannerView channelBannerView, Bitmap bitmap) { - if (bitmap != null && info.getId() - .equals(channelBannerView.mCurrentChannel.getInputId())) { + if (bitmap != null && channelBannerView.mCurrentChannel != null + && info.getId().equals(channelBannerView.mCurrentChannel.getInputId())) { channelBannerView.updateTvInputLogo(bitmap); } } @@ -510,7 +538,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage if (mLastUpdatedProgram == null || !TextUtils.equals(program.getTitle(), mLastUpdatedProgram.getTitle()) || !TextUtils.equals(program.getEpisodeDisplayTitle(getContext()), - mLastUpdatedProgram.getEpisodeDisplayTitle(getContext()))) { + mLastUpdatedProgram.getEpisodeDisplayTitle(getContext()))) { updateProgramTextView(program); } updateProgramTimeInfo(program); @@ -519,7 +547,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage // cancel the animation. boolean isProgramChanged = !Objects.equals(mLastUpdatedProgram, program); if (mResizeAnimator != null && isProgramChanged) { - mLastUpdatedProgram = program; + setLastUpdatedProgram(program); mProgramInfoUpdatePendingByResizing = true; mResizeAnimator.cancel(); } else if (mResizeAnimator == null) { @@ -537,16 +565,73 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage } else { mProgramInfoUpdatePendingByResizing = true; } - mLastUpdatedProgram = program; + setLastUpdatedProgram(program); + } + + private void updateProgramInfo(RecordedProgram recordedProgram) { + if (mLockType == LOCK_CHANNEL_INFO) { + updateProgramInfo(sLockedChannelProgram); + return; + } else if (recordedProgram == null) { + updateProgramInfo(sNoProgram); + return; + } + + if (mLastUpdatedRecordedProgram == null + || !TextUtils.equals(recordedProgram.getTitle(), + mLastUpdatedRecordedProgram.getTitle()) + || !TextUtils.equals(recordedProgram.getEpisodeDisplayTitle(getContext()), + mLastUpdatedRecordedProgram.getEpisodeDisplayTitle(getContext()))) { + updateProgramTextView(recordedProgram); + } + updateProgramTimeInfo(recordedProgram); + + // When the program is changed, but the previous resize animation has not ended yet, + // cancel the animation. + boolean isProgramChanged = !Objects.equals(mLastUpdatedRecordedProgram, recordedProgram); + if (mResizeAnimator != null && isProgramChanged) { + setLastUpdatedRecordedProgram(recordedProgram); + mProgramInfoUpdatePendingByResizing = true; + mResizeAnimator.cancel(); + } else if (mResizeAnimator == null) { + if (mLockType != LOCK_NONE + || TextUtils.isEmpty(recordedProgram.getShortDescription())) { + mProgramDescriptionTextView.setVisibility(GONE); + mProgramDescriptionText = ""; + } else { + mProgramDescriptionTextView.setVisibility(VISIBLE); + mProgramDescriptionText = recordedProgram.getShortDescription(); + } + String description = mProgramDescriptionTextView.getText().toString(); + boolean needFadeAnimation = isProgramChanged + || !description.equals(mProgramDescriptionText); + updateBannerHeight(needFadeAnimation); + } else { + mProgramInfoUpdatePendingByResizing = true; + } + setLastUpdatedRecordedProgram(recordedProgram); } private void updateProgramTextView(Program program) { if (program == null) { return; } + updateProgramTextView(program == sLockedChannelProgram, program.getTitle(), + program.getEpisodeTitle(), program.getEpisodeDisplayTitle(getContext())); + } + + private void updateProgramTextView(RecordedProgram recordedProgram) { + if (recordedProgram == null) { + return; + } + updateProgramTextView(false, recordedProgram.getTitle(), recordedProgram.getEpisodeTitle(), + recordedProgram.getEpisodeDisplayTitle(getContext())); + } + private void updateProgramTextView(boolean dimText, String title, String episodeTitle, + String episodeDisplayTitle) { mProgramTextView.setVisibility(View.VISIBLE); - if (program == sLockedChannelProgram) { + if (dimText) { mProgramTextView.setTextColor(mChannelBannerDimTextColor); } else { mProgramTextView.setTextColor(mChannelBannerTextColor); @@ -554,17 +639,15 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage updateTextView(mProgramTextView, R.dimen.channel_banner_program_large_text_size, R.dimen.channel_banner_program_large_margin_top); - if (TextUtils.isEmpty(program.getEpisodeTitle())) { - mProgramTextView.setText(program.getTitle()); + if (TextUtils.isEmpty(episodeTitle)) { + mProgramTextView.setText(title); } else { - String title = program.getTitle(); - String episodeTitle = program.getEpisodeDisplayTitle(getContext()); - String fullTitle = title + " " + episodeTitle; + String fullTitle = title + " " + episodeDisplayTitle; SpannableString text = new SpannableString(fullTitle); text.setSpan(new TextAppearanceSpan(getContext(), R.style.text_appearance_channel_banner_episode_title), - fullTitle.length() - episodeTitle.length(), fullTitle.length(), + fullTitle.length() - episodeDisplayTitle.length(), fullTitle.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); mProgramTextView.setText(text); } @@ -617,6 +700,38 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage } } + private void updateProgramTimeInfo(RecordedProgram recordedProgram) { + long durationMs = recordedProgram.getDurationMillis(); + if (mLockType != LOCK_CHANNEL_INFO && durationMs > 0) { + mProgramTimeTextView.setVisibility(View.VISIBLE); + mRemainingTimeView.setVisibility(View.VISIBLE); + + mProgramTimeTextView.setText(DateUtils.formatElapsedTime(durationMs / 1000)); + + long currTimeMs = mMainActivity.getCurrentPlayingPosition(); + if (currTimeMs <= 0) { + mRemainingTimeView.setProgress(0); + } else if (currTimeMs >= durationMs) { + mRemainingTimeView.setProgress(100); + } else { + mRemainingTimeView.setProgress((int) (100 * currTimeMs / durationMs)); + } + } else { + mProgramTimeTextView.setVisibility(View.GONE); + mRemainingTimeView.setVisibility(View.GONE); + } + } + + private void setLastUpdatedProgram(Program program) { + mLastUpdatedProgram = program; + mLastUpdatedRecordedProgram = null; + } + + private void setLastUpdatedRecordedProgram(RecordedProgram recordedProgram) { + mLastUpdatedProgram = null; + mLastUpdatedRecordedProgram = recordedProgram; + } + private void updateBannerHeight(boolean needFadeAnimation) { Assert.assertNull(mResizeAnimator); // Need to measure the layout height with the new description text. diff --git a/src/com/android/tv/ui/DialogUtils.java b/src/com/android/tv/ui/DialogUtils.java new file mode 100644 index 00000000..acbaf8c8 --- /dev/null +++ b/src/com/android/tv/ui/DialogUtils.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.ui; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Resources; + +import com.android.tv.common.SoftPreconditions; + +public final class DialogUtils { + + /** + * Shows a list in a Dialog. + * + * @param itemResIds String resource id for each item + * @param runnables Runnable for each item + */ + public static void showListDialog(Context context, int[] itemResIds, + final Runnable[] runnables) { + int size = itemResIds.length; + SoftPreconditions.checkState(size == runnables.length); + DialogInterface.OnClickListener onClickListener + = new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, int which) { + Runnable runnable = runnables[which]; + if (runnable != null) { + runnable.run(); + } + dialog.dismiss(); + } + }; + CharSequence[] items = new CharSequence[itemResIds.length]; + Resources res = context.getResources(); + for (int i = 0; i < size; ++i) { + items[i] = res.getString(itemResIds[i]); + } + new AlertDialog.Builder(context) + .setItems(items, onClickListener) + .create() + .show(); + } + + private DialogUtils() { } +} diff --git a/src/com/android/tv/ui/KeypadChannelSwitchView.java b/src/com/android/tv/ui/KeypadChannelSwitchView.java index cf43fc9b..abc05bad 100644 --- a/src/com/android/tv/ui/KeypadChannelSwitchView.java +++ b/src/com/android/tv/ui/KeypadChannelSwitchView.java @@ -41,9 +41,9 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.Tracker; +import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.ChannelNumber; -import com.android.tv.util.SoftPreconditions; import java.util.ArrayList; import java.util.List; diff --git a/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java b/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java index afea9ba5..63ee199d 100644 --- a/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java +++ b/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java @@ -35,8 +35,8 @@ public class OnRepeatedKeyInterceptListener implements VerticalGridView.OnKeyInt private static final int[] MAX_SKIPPED_VIEW_COUNT = { 1, 4 }; private static final int MSG_MOVE_FOCUS = 1000; - private VerticalGridView mView; - private MyHandler mHandler = new MyHandler(this); + private final VerticalGridView mView; + private final MyHandler mHandler = new MyHandler(this); private int mDirection; private boolean mFocusAccelerated; private long mRepeatedKeyInterval; diff --git a/src/com/android/tv/ui/SelectInputView.java b/src/com/android/tv/ui/SelectInputView.java index 032782bd..646f9159 100644 --- a/src/com/android/tv/ui/SelectInputView.java +++ b/src/com/android/tv/ui/SelectInputView.java @@ -249,14 +249,16 @@ public class SelectInputView extends VerticalGridView implements boolean foundTuner = false; for (TvInputInfo input : mTvInputManagerHelper.getTvInputInfos(false, false)) { if (input.isPassthroughInput()) { - mInputList.add(input); - inputMap.put(input.getId(), input); + if (!input.isHidden(getContext())) { + mInputList.add(input); + inputMap.put(input.getId(), input); + } } else if (!foundTuner) { foundTuner = true; mInputList.add(input); } } - // Do not show an AVR if an HDMI device is connected to it. + // Do not show HDMI ports if a CEC device is directly connected to the port. for (TvInputInfo input : inputMap.values()) { if (input.getParentId() != null && !input.isConnectedToHdmiSwitch()) { mInputList.remove(inputMap.get(input.getParentId())); diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java index 286bc1f9..6d3d62aa 100644 --- a/src/com/android/tv/ui/TunableTvView.java +++ b/src/com/android/tv/ui/TunableTvView.java @@ -17,11 +17,11 @@ package com.android.tv.ui; import android.animation.Animator; -import android.animation.AnimatorInflater; import android.animation.AnimatorListenerAdapter; import android.animation.TimeInterpolator; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.content.ContentUris; import android.content.Context; import android.content.pm.PackageManager; import android.media.PlaybackParams; @@ -29,13 +29,18 @@ import android.media.tv.TvContentRating; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvTrackInfo; +import android.media.tv.TvView; import android.media.tv.TvView.OnUnhandledInputEventListener; import android.media.tv.TvView.TvInputCallback; +import android.net.ConnectivityManager; import android.net.Uri; +import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.support.annotation.IntDef; import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v4.os.BuildCompat; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; @@ -46,50 +51,57 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; -import android.widget.TextView; import com.android.tv.ApplicationSingletons; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.Tracker; -import com.android.tv.common.recording.PlaybackTvView; -import com.android.tv.common.recording.RecordingUtils; +import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.common.recording.RecordedProgram; import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; import com.android.tv.data.StreamInfo; import com.android.tv.data.WatchedHistoryManager; +import com.android.tv.dvr.DvrDataManager; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.recommendation.NotificationService; +import com.android.tv.util.NetworkUtils; import com.android.tv.util.PermissionUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.List; public class TunableTvView extends FrameLayout implements StreamInfo { private static final boolean DEBUG = false; private static final String TAG = "TunableTvView"; - public static final String PERMISSION_RECEIVE_INPUT_EVENT = - "com.android.tv.permission.RECEIVE_INPUT_EVENT"; - public static final int VIDEO_UNAVAILABLE_REASON_NOT_TUNED = -1; + @Retention(RetentionPolicy.SOURCE) + @IntDef({BLOCK_SCREEN_TYPE_NO_UI, BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW, BLOCK_SCREEN_TYPE_NORMAL}) + public @interface BlockScreenType {} public static final int BLOCK_SCREEN_TYPE_NO_UI = 0; public static final int BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW = 1; public static final int BLOCK_SCREEN_TYPE_NORMAL = 2; + private static final String PERMISSION_RECEIVE_INPUT_EVENT = + "com.android.tv.permission.RECEIVE_INPUT_EVENT"; + @Retention(RetentionPolicy.SOURCE) @IntDef({ TIME_SHIFT_STATE_NONE, TIME_SHIFT_STATE_PLAY, TIME_SHIFT_STATE_PAUSE, - TIME_SHIFT_STATE_REWIND, TIME_SHIFT_STATE_FAST_FORWARD }) - public @interface TimeShiftState {} - public static final int TIME_SHIFT_STATE_NONE = 0; - public static final int TIME_SHIFT_STATE_PLAY = 1; - public static final int TIME_SHIFT_STATE_PAUSE = 2; - public static final int TIME_SHIFT_STATE_REWIND = 3; - public static final int TIME_SHIFT_STATE_FAST_FORWARD = 4; + TIME_SHIFT_STATE_REWIND, TIME_SHIFT_STATE_FAST_FORWARD }) + private @interface TimeShiftState {} + private static final int TIME_SHIFT_STATE_NONE = 0; + private static final int TIME_SHIFT_STATE_PLAY = 1; + private static final int TIME_SHIFT_STATE_PAUSE = 2; + private static final int TIME_SHIFT_STATE_REWIND = 3; + private static final int TIME_SHIFT_STATE_FAST_FORWARD = 4; private static final int FADED_IN = 0; private static final int FADED_OUT = 1; @@ -103,6 +115,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private AppLayerTvView mTvView; private Channel mCurrentChannel; + private RecordedProgram mRecordedProgram; private TvInputManagerHelper mInputManagerHelper; private ContentRatingsManager mContentRatingsManager; @Nullable @@ -114,6 +127,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private int mVideoHeight; private int mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; private float mVideoFrameRate; + private float mVideoDisplayAspectRatio; private int mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; private boolean mHasClosedCaption = false; private boolean mVideoAvailable; @@ -130,7 +144,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private boolean mIsPip; private int mScreenHeight; private int mShrunkenTvViewHeight; - private boolean mCanModifyParentalControls; + private final boolean mCanModifyParentalControls; @TimeShiftState private int mTimeShiftState = TIME_SHIFT_STATE_NONE; private TimeShiftListener mTimeShiftListener; @@ -139,22 +153,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private final Tracker mTracker; private final DurationTimer mChannelViewTimer = new DurationTimer(); + private InternetCheckTask mInternetCheckTask; // A block screen view which has lock icon with black background. // This indicates that user's action is needed to play video. - private final View mBlockScreenView; - - private final View mBlockScreenDescriptionView; - private final ImageView mBlockScreenIconView; - private final View mBlockScreenShrunkenIconView; - private final TextView mBlockScreenTextView; - - // Animators used for fade in/out of block screen icon. - private final Animator mBlockScreenDescriptionFadeIn; - private final Animator mBlockScreenDescriptionFadeOut; + private final BlockScreenView mBlockScreenView; // A View to hide screen when there's problem in video playback. - private final TextView mHideScreenView; + private final BlockScreenView mHideScreenView; // A View to block screen until onContentAllowed is received if parental control is on. private final View mBlockScreenForTuneView; @@ -167,7 +173,11 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private int mFadeState = FADED_IN; private Runnable mActionAfterFade; - private int mBlockScreenType; + @BlockScreenType private int mBlockScreenType; + + private final DvrDataManager mDvrDataManager; + private final ChannelDataManager mChannelDataManager; + private final ConnectivityManager mConnectivityManager; private final TvInputCallback mCallback = new TvInputCallback() { @@ -239,6 +249,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mVideoHeight = 0; mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; mVideoFrameRate = 0f; + mVideoDisplayAspectRatio = 0f; } else if (type == TvTrackInfo.TYPE_AUDIO) { mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; } @@ -254,6 +265,18 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mVideoFormat = Utils.getVideoDefinitionLevelFromSize( mVideoWidth, mVideoHeight); mVideoFrameRate = track.getVideoFrameRate(); + if (mVideoWidth <= 0 || mVideoHeight <= 0) { + mVideoDisplayAspectRatio = 0.0f; + } else if (android.os.Build.VERSION.SDK_INT >= + android.os.Build.VERSION_CODES.M) { + float VideoPixelAspectRatio = + track.getVideoPixelAspectRatio(); + mVideoDisplayAspectRatio = VideoPixelAspectRatio + * mVideoWidth / mVideoHeight; + } else { + mVideoDisplayAspectRatio = mVideoWidth + / (float) mVideoHeight; + } } else if (type == TvTrackInfo.TYPE_AUDIO) { mAudioChannelCount = track.getAudioChannelCount(); } @@ -315,7 +338,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo { @Override @TargetApi(Build.VERSION_CODES.M) public void onTimeShiftStatusChanged(String inputId, int status) { - setTimeShiftAvailable(status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE); + boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE; + setTimeShiftAvailable(available); } }; @@ -336,53 +360,32 @@ public class TunableTvView extends FrameLayout implements StreamInfo { inflate(getContext(), R.layout.tunable_tv_view, this); ApplicationSingletons appSingletons = TvApplication.getSingletons(context); + mDvrDataManager = CommonFeatures.DVR.isEnabled(context) && BuildCompat.isAtLeastN() + ? appSingletons.getDvrDataManager() + : null; + mChannelDataManager = appSingletons.getChannelDataManager(); + mConnectivityManager = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); mCanModifyParentalControls = PermissionUtils.hasModifyParentalControls(context); mTracker = appSingletons.getTracker(); mBlockScreenType = BLOCK_SCREEN_TYPE_NORMAL; - mBlockScreenView = findViewById(R.id.block_screen); - mBlockScreenDescriptionView = findViewById(R.id.block_screen_description); - - mBlockScreenIconView = (ImageView) mBlockScreenView.findViewById(R.id.block_screen_icon); + mBlockScreenView = (BlockScreenView) findViewById(R.id.block_screen); if (!mCanModifyParentalControls) { - mBlockScreenIconView.setImageResource(R.drawable.ic_message_lock_no_permission); - mBlockScreenIconView.setScaleType(ImageView.ScaleType.CENTER); + mBlockScreenView.setImage(R.drawable.ic_message_lock_no_permission); + mBlockScreenView.setScaleType(ImageView.ScaleType.CENTER); + } else { + mBlockScreenView.setImage(R.drawable.ic_message_lock); } - mBlockScreenShrunkenIconView = mBlockScreenView.findViewById( - R.id.block_screen_shrunken_icon); - mBlockScreenTextView = (TextView) mBlockScreenView.findViewById(R.id.block_screen_text); - - mBlockScreenDescriptionFadeIn = AnimatorInflater.loadAnimator(context, - R.animator.tvview_block_screen_fade_in); - mBlockScreenDescriptionFadeIn.setTarget(mBlockScreenDescriptionView); - mBlockScreenDescriptionFadeIn.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - switch (mBlockScreenType) { - case BLOCK_SCREEN_TYPE_NORMAL: - mBlockScreenIconView.setVisibility(VISIBLE); - mBlockScreenShrunkenIconView.setVisibility(GONE); - break; - case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW: - mBlockScreenIconView.setVisibility(GONE); - mBlockScreenShrunkenIconView.setVisibility(VISIBLE); - break; - } - mBlockScreenDescriptionView.setVisibility(VISIBLE); - } - }); - mBlockScreenDescriptionFadeOut = AnimatorInflater.loadAnimator(context, - R.animator.tvview_block_screen_fade_out); - mBlockScreenDescriptionFadeOut.setTarget(mBlockScreenDescriptionView); - mBlockScreenDescriptionFadeOut.addListener(new AnimatorListenerAdapter() { + mBlockScreenView.setShrunkenImage(R.drawable.ic_message_lock_preview); + mBlockScreenView.addFadeOutAnimationListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - mBlockScreenDescriptionView.setVisibility(GONE); - mBlockScreenDescriptionView.setAlpha(1f); - updateBlockScreenTextView(); + adjustBlockScreenSpacingAndText(); } }); - mHideScreenView = (TextView) findViewById(R.id.hide_screen); + mHideScreenView = (BlockScreenView) findViewById(R.id.hide_screen); + mHideScreenView.setImageVisibility(false); mBufferingSpinnerView = findViewById(R.id.buffering_spinner); mBlockScreenForTuneView = findViewById(R.id.block_screen_for_tune); mDimScreenView = findViewById(R.id.dim); @@ -441,6 +444,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { public void reset() { mTvView.reset(); mCurrentChannel = null; + mRecordedProgram = null; mInputInfo = null; mCanReceiveInputEvent = false; mOnTuneListener = null; @@ -471,15 +475,82 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } /** + * Returns {@code true}, if this view is the recording playback mode. + */ + public boolean isRecordingPlayback() { + return mRecordedProgram != null; + } + + /** + * Returns the recording which is being played right now. + */ + public RecordedProgram getPlayingRecordedProgram() { + return mRecordedProgram; + } + + /** * Plays a recording. */ - public boolean playRecording(String inputId, Uri recordingUri, OnTuneListener listener) { - // Create a dummy channel. - Channel channel = new Channel.Builder() - .setId(0) - .setInputId(inputId) - .build(); - return tuneTo(channel, RecordingUtils.buildMediaUri(recordingUri), listener); + public boolean playRecording(Uri recordingUri, OnTuneListener listener) { + if (!mStarted) { + throw new IllegalStateException("TvView isn't started"); + } + if (!CommonFeatures.DVR.isEnabled(getContext()) || !BuildCompat.isAtLeastN()) { + return false; + } + if (DEBUG) Log.d(TAG, "playRecording " + recordingUri); + long recordingId = ContentUris.parseId(recordingUri); + mRecordedProgram = mDvrDataManager.getRecordedProgram(recordingId); + if (mRecordedProgram == null) { + Log.w(TAG, "No recorded program (Uri=" + recordingUri + ")"); + return false; + } + String inputId = mRecordedProgram.getInputId(); + TvInputInfo inputInfo = mInputManagerHelper.getTvInputInfo(inputId); + if (inputInfo == null) { + return false; + } + mOnTuneListener = listener; + // mCurrentChannel can be null. + mCurrentChannel = mChannelDataManager.getChannel(mRecordedProgram.getChannelId()); + // For recording playback, input event should not be sent. + mCanReceiveInputEvent = false; + boolean needSurfaceSizeUpdate = false; + if (!inputInfo.equals(mInputInfo)) { + mInputInfo = inputInfo; + if (DEBUG) { + Log.d(TAG, "Input \'" + mInputInfo.getId() + "\' can receive input event: " + + mCanReceiveInputEvent); + } + needSurfaceSizeUpdate = true; + } + mChannelViewTimer.start(); + mVideoWidth = 0; + mVideoHeight = 0; + mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; + mVideoFrameRate = 0f; + mVideoDisplayAspectRatio = 0f; + mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; + mHasClosedCaption = false; + mTvView.setCallback(mCallback); + mTimeShiftCurrentPositionMs = INVALID_TIME; + mTvView.setTimeShiftPositionCallback(null); + setTimeShiftAvailable(false); + mTvView.timeShiftPlay(inputId, recordingUri); + if (needSurfaceSizeUpdate && mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) { + // When the input is changed, TvView recreates its SurfaceView internally. + // So we need to call SurfaceHolder.setFixedSize for the new SurfaceView. + getSurfaceView().getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight); + } + hideScreenByVideoAvailability(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); + unblockScreenByContentRating(); + if (mParentControlEnabled) { + mBlockScreenForTuneView.setVisibility(View.VISIBLE); + } + if (mOnTuneListener != null) { + mOnTuneListener.onStreamInfoChanged(this); + } + return true; } /** @@ -508,6 +579,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } mOnTuneListener = listener; mCurrentChannel = channel; + mRecordedProgram = null; boolean tunedByRecommendation = params != null && params.getString(NotificationService.TUNE_PARAMS_RECOMMENDATION_TYPE) != null; boolean needSurfaceSizeUpdate = false; @@ -528,6 +600,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mVideoHeight = 0; mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; mVideoFrameRate = 0f; + mVideoDisplayAspectRatio = 0f; mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; mHasClosedCaption = false; mTvView.setCallback(mCallback); @@ -590,8 +663,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { * Note: Once {@link android.view.SurfaceHolder#setFixedSize} is called, * {@link android.view.SurfaceView} and its underlying window can be misaligned, when the size * of {@link android.view.SurfaceView} is changed without changing either left position or top - * position. For detail, please refer the codes of {@link android.view.SurfaceView#updateWindow} - * . + * position. For detail, please refer the codes of android.view.SurfaceView.updateWindow(). */ public void setFixedSurfaceSize(int width, int height) { mFixedSurfaceWidth = width; @@ -636,8 +708,18 @@ public class TunableTvView extends FrameLayout implements StreamInfo { void onContentAllowed(); } - public void requestUnblockContent(TvContentRating rating) { - mTvView.unblockContent(rating); + public void unblockContent(TvContentRating rating) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + try { + Method method = TvView.class.getMethod("requestUnblockContent", + TvContentRating.class); + method.invoke(mTvView, rating); + } catch (NoSuchMethodException|IllegalAccessException|InvocationTargetException e) { + e.printStackTrace(); + } + } else { + mTvView.unblockContent(rating); + } } @Override @@ -660,6 +742,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo { return mVideoFrameRate; } + /** + * Returns displayed aspect ratio (video width / video height * pixel ratio). + */ + @Override + public float getVideoDisplayAspectRatio() { + return mVideoDisplayAspectRatio; + } + @Override public int getAudioChannelCount() { return mAudioChannelCount; @@ -683,7 +773,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { /** * Returns the {@link android.view.SurfaceView} of the {@link android.media.tv.TvView}. */ - public SurfaceView getSurfaceView() { + private SurfaceView getSurfaceView() { return (SurfaceView) mTvView.getChildAt(0); } @@ -757,8 +847,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { scale = height * PIP_BLOCK_SCREEN_SCALE_FACTOR / mScreenHeight; } // TODO: need to get UX confirmation. - mBlockScreenDescriptionView.setScaleX(scale); - mBlockScreenDescriptionView.setScaleY(scale); + mBlockScreenView.scaleContainerView(scale); } } @@ -817,7 +906,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { * * @param type The type of block screen to set. */ - public void setBlockScreenType(int type) { + public void setBlockScreenType(@BlockScreenType int type) { // TODO: need to support the transition from NORMAL to SHRUNKEN and vice verse. if (mBlockScreenType != type) { mBlockScreenType = type; @@ -826,12 +915,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } private void updateBlockScreenUI(boolean animation) { - if (mBlockScreenDescriptionFadeIn.isRunning()) { - mBlockScreenDescriptionFadeIn.end(); - } - if (mBlockScreenDescriptionFadeOut.isRunning()) { - mBlockScreenDescriptionFadeOut.end(); - } + mBlockScreenView.endAnimations(); if (!mScreenBlocked && mBlockedContentRating == null) { mBlockScreenView.setVisibility(GONE); @@ -839,101 +923,72 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } mBlockScreenView.setVisibility(VISIBLE); - if (!animation) { - updateBlockScreenTextView(); - switch (mBlockScreenType) { - case BLOCK_SCREEN_TYPE_NO_UI: - mBlockScreenIconView.setVisibility(GONE); - mBlockScreenShrunkenIconView.setVisibility(GONE); - mBlockScreenDescriptionView.setVisibility(GONE); - break; - case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW: - mBlockScreenIconView.setVisibility(GONE); - mBlockScreenShrunkenIconView.setVisibility(VISIBLE); - mBlockScreenDescriptionView.setVisibility(VISIBLE); - break; - case BLOCK_SCREEN_TYPE_NORMAL: - mBlockScreenIconView.setVisibility(VISIBLE); - mBlockScreenShrunkenIconView.setVisibility(GONE); - mBlockScreenDescriptionView.setVisibility(VISIBLE); - break; - } - } else { - switch (mBlockScreenType) { - case BLOCK_SCREEN_TYPE_NO_UI: - if (mBlockScreenDescriptionView.getVisibility() == VISIBLE) { - mBlockScreenDescriptionFadeOut.start(); - } - break; - case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW: - case BLOCK_SCREEN_TYPE_NORMAL: - updateBlockScreenTextView(); - if (mBlockScreenDescriptionView.getVisibility() == GONE) { - mBlockScreenDescriptionFadeIn.start(); - } - break; - } + if (!animation || mBlockScreenType != TunableTvView.BLOCK_SCREEN_TYPE_NO_UI) { + adjustBlockScreenSpacingAndText(); } + mBlockScreenView.onBlockStatusChanged(mBlockScreenType, animation); } - private void updateBlockScreenTextView() { + private void adjustBlockScreenSpacingAndText() { // TODO: need to add animation for padding change when the block screen type is changed // NORMAL to SHRUNKEN and vice verse. - mBlockScreenTextView.setPadding(0, - getResources().getDimensionPixelOffset( - mBlockScreenType == BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW - ? R.dimen.shrunken_tvview_block_text_padding_top - : R.dimen.tvview_block_text_padding_top), - 0, 0); + mBlockScreenView.setSpacing(mBlockScreenType); + String text = getBlockScreenText(); + if (text != null) { + mBlockScreenView.setText(text); + } + } + /** + * Returns the block screen text corresponding to the current status. + * Note that returning {@code null} value means that the current text should not be changed. + */ + private String getBlockScreenText() { if (mScreenBlocked) { switch (mBlockScreenType) { case BLOCK_SCREEN_TYPE_NO_UI: case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW: - mBlockScreenTextView.setText(""); - break; + return ""; case BLOCK_SCREEN_TYPE_NORMAL: if (mCanModifyParentalControls) { - mBlockScreenTextView.setText(R.string.tvview_channel_locked); + return getResources().getString(R.string.tvview_channel_locked); } else { - mBlockScreenTextView.setText(R.string.tvview_channel_locked_no_permission); + return getResources().getString( + R.string.tvview_channel_locked_no_permission); } - break; } } else if (mBlockedContentRating != null) { String name = mContentRatingsManager.getDisplayNameForRating(mBlockedContentRating); switch (mBlockScreenType) { case BLOCK_SCREEN_TYPE_NO_UI: - mBlockScreenTextView.setText(""); - break; + return ""; case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW: if (TextUtils.isEmpty(name)) { - mBlockScreenTextView.setText(R.string.shrunken_tvview_content_locked); + return getResources().getString(R.string.shrunken_tvview_content_locked); } else { - mBlockScreenTextView.setText(getContext().getString( - R.string.shrunken_tvview_content_locked_format, name)); + return getContext().getString( + R.string.shrunken_tvview_content_locked_format, name); } - break; case BLOCK_SCREEN_TYPE_NORMAL: if (TextUtils.isEmpty(name)) { if (mCanModifyParentalControls) { - mBlockScreenTextView.setText(R.string.tvview_content_locked); + return getResources().getString(R.string.tvview_content_locked); } else { - mBlockScreenTextView.setText( + return getResources().getString( R.string.tvview_content_locked_no_permission); } } else { if (mCanModifyParentalControls) { - mBlockScreenTextView.setText(getContext().getString( - R.string.tvview_content_locked_format, name)); + return getContext().getString( + R.string.tvview_content_locked_format, name); } else { - mBlockScreenTextView.setText(getContext().getString( - R.string.tvview_content_locked_format_no_permission, name)); + return getContext().getString( + R.string.tvview_content_locked_format_no_permission, name); } } - break; } } + return null; } private void checkBlockScreenAndMuteNeeded() { @@ -968,10 +1023,18 @@ public class TunableTvView extends FrameLayout implements StreamInfo { checkBlockScreenAndMuteNeeded(); } + @UiThread private void hideScreenByVideoAvailability(int reason) { + mVideoAvailable = false; + mVideoUnavailableReason = reason; + if (mInternetCheckTask != null) { + mInternetCheckTask.cancel(true); + mInternetCheckTask = null; + } switch (reason) { case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY: mHideScreenView.setVisibility(VISIBLE); + mHideScreenView.setImageVisibility(false); mHideScreenView.setText(R.string.tvview_msg_audio_only); mBufferingSpinnerView.setVisibility(GONE); unmuteIfPossible(); @@ -980,19 +1043,33 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mBufferingSpinnerView.setVisibility(VISIBLE); mute(); break; - case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: - case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING: case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: + mHideScreenView.setVisibility(VISIBLE); + mHideScreenView.setText(R.string.tvview_msg_weak_signal); + mBufferingSpinnerView.setVisibility(GONE); + mute(); + break; + case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING: case VIDEO_UNAVAILABLE_REASON_NOT_TUNED: + mHideScreenView.setVisibility(VISIBLE); + mHideScreenView.setImageVisibility(false); + mHideScreenView.setText(null); + mBufferingSpinnerView.setVisibility(GONE); + mute(); + break; + case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: default: mHideScreenView.setVisibility(VISIBLE); + mHideScreenView.setImageVisibility(false); mHideScreenView.setText(null); mBufferingSpinnerView.setVisibility(GONE); mute(); + if (mCurrentChannel != null && !mCurrentChannel.isPhysicalTunerChannel()) { + mInternetCheckTask = new InternetCheckTask(); + mInternetCheckTask.execute(); + } break; } - mVideoAvailable = false; - mVideoUnavailableReason = reason; } private void unhideScreenByVideoAvailability() { @@ -1094,7 +1171,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } mTimeShiftAvailable = isTimeShiftAvailable; if (isTimeShiftAvailable) { - mTvView.setTimeShiftPositionCallback(new PlaybackTvView.TimeShiftPositionCallback2() { + mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() { @Override public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { if (mTimeShiftListener != null && mCurrentChannel != null @@ -1107,14 +1184,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo { public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { mTimeShiftCurrentPositionMs = timeMs; } - - @Override - public void onTimeShiftEndPositionChanged(String inputId, long timeMs) { - if (mTimeShiftListener != null && mCurrentChannel != null - && mCurrentChannel.getInputId().equals(inputId)) { - mTimeShiftListener.onRecordEndTimeChanged(timeMs); - } - } }); } else { mTvView.setTimeShiftPositionCallback(null); @@ -1265,11 +1334,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo { * Called when the record start time has been changed. */ public abstract void onRecordStartTimeChanged(long recordStartTimeMs); - - /** - * Called when the record end time has been changed. - */ - public abstract void onRecordEndTimeChanged(long recordEndTimeMs); } /** @@ -1281,4 +1345,22 @@ public class TunableTvView extends FrameLayout implements StreamInfo { */ public abstract void onScreenBlockingChanged(boolean blocked); } + + public class InternetCheckTask extends AsyncTask<Void, Void, Boolean> { + @Override + protected Boolean doInBackground(Void... params) { + return NetworkUtils.isNetworkAvailable(mConnectivityManager); + } + + @Override + protected void onPostExecute(Boolean networkAvailable) { + mInternetCheckTask = null; + if (!mVideoAvailable && !networkAvailable && isAttachedToWindow() + && mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN) { + mHideScreenView.setImageVisibility(true); + mHideScreenView.setImage(R.drawable.ic_sad_cloud); + mHideScreenView.setText(R.string.tvview_msg_no_internet_connection); + } + } + } } diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java index 124f3393..94f9b0f9 100644 --- a/src/com/android/tv/ui/TvOverlayManager.java +++ b/src/com/android/tv/ui/TvOverlayManager.java @@ -29,6 +29,7 @@ import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; +import android.support.v4.os.BuildCompat; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; @@ -46,6 +47,7 @@ import com.android.tv.TimeShiftManager; import com.android.tv.TvApplication; import com.android.tv.analytics.Tracker; import com.android.tv.common.WeakHandler; +import com.android.tv.common.feature.CommonFeatures; import com.android.tv.common.ui.setup.OnActionClickListener; import com.android.tv.common.ui.setup.SetupFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; @@ -54,7 +56,9 @@ import com.android.tv.dialog.FullscreenDialogFragment; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dialog.RecentlyWatchedDialogFragment; import com.android.tv.dialog.SafeDismissDialogFragment; +import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.ui.DvrActivity; +import com.android.tv.dvr.ui.HalfSizedDialogFragment; import com.android.tv.guide.ProgramGuide; import com.android.tv.menu.Menu; import com.android.tv.menu.Menu.MenuShowReason; @@ -133,6 +137,7 @@ public class TvOverlayManager { AVAILABLE_DIALOG_TAGS.add(FullscreenDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(SettingsFragment.LicenseActionItem.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(RatingsFragment.AttributionItem.DIALOG_TAG); + AVAILABLE_DIALOG_TAGS.add(HalfSizedDialogFragment.DIALOG_TAG); } private final MainActivity mMainActivity; @@ -155,7 +160,7 @@ public class TvOverlayManager { private @TvOverlayType int mOpenedOverlays; - private List<Runnable> mPendingActions = new ArrayList<>(); + private final List<Runnable> mPendingActions = new ArrayList<>(); public TvOverlayManager(MainActivity mainActivity, ChannelTuner channelTuner, KeypadChannelSwitchView keypadChannelSwitchView, @@ -227,9 +232,13 @@ public class TvOverlayManager { onOverlayClosed(OVERLAY_TYPE_GUIDE); } }; + DvrDataManager dvrDataManager = + CommonFeatures.DVR.isEnabled(mainActivity) && BuildCompat.isAtLeastN() ? singletons + .getDvrDataManager() : null; mProgramGuide = new ProgramGuide(mainActivity, channelTuner, singletons.getTvInputManagerHelper(), mChannelDataManager, - singletons.getProgramDataManager(), singletons.getTracker(), preShowRunnable, + singletons.getProgramDataManager(), dvrDataManager, singletons.getTracker(), + preShowRunnable, postHideRunnable); mSetupFragment = new SetupSourcesFragment(); mSetupFragment.setOnActionClickListener(new OnActionClickListener() { @@ -346,10 +355,18 @@ public class TvOverlayManager { */ public void showDialogFragment(String tag, SafeDismissDialogFragment dialog, boolean keepSidePanelHistory) { + showDialogFragment(tag, dialog, keepSidePanelHistory, false); + } + + public void showDialogFragment(String tag, SafeDismissDialogFragment dialog, + boolean keepSidePanelHistory, boolean keepProgramGuide) { int flags = FLAG_HIDE_OVERLAYS_KEEP_DIALOG; if (keepSidePanelHistory) { flags |= FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY; } + if (keepProgramGuide) { + flags |= FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE; + } hideOverlays(flags); // A tag for dialog must be added to AVAILABLE_DIALOG_TAGS to make it launchable from TV. if (!AVAILABLE_DIALOG_TAGS.contains(tag)) { @@ -422,7 +439,6 @@ public class TvOverlayManager { public void showSetupFragment() { if (DEBUG) Log.d(TAG, "showSetupFragment"); mSetupFragmentActive = true; - SetupSourcesFragment.setTheme(R.style.Theme_TV_GuidedStep); mSetupFragment.enableFragmentTransition(SetupFragment.FRAGMENT_ENTER_TRANSITION | SetupFragment.FRAGMENT_EXIT_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION | SetupFragment.FRAGMENT_REENTER_TRANSITION); @@ -437,7 +453,6 @@ public class TvOverlayManager { return; } mSetupFragmentActive = false; - SetupSourcesFragment.setTheme(SetupSourcesFragment.DEFAULT_THEME); closeFragment(removeFragment ? mSetupFragment : null); if (mChannelDataManager.getChannelCount() == 0) { mMainActivity.finish(); diff --git a/src/com/android/tv/ui/TvTransitionManager.java b/src/com/android/tv/ui/TvTransitionManager.java index 444b5c0c..52e96cc0 100644 --- a/src/com/android/tv/ui/TvTransitionManager.java +++ b/src/com/android/tv/ui/TvTransitionManager.java @@ -33,6 +33,7 @@ import android.widget.FrameLayout.LayoutParams; import com.android.tv.MainActivity; import com.android.tv.R; +import com.android.tv.data.Channel; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -89,15 +90,21 @@ public class TvTransitionManager extends TransitionManager { } initIfNeeded(); if (withAnimation) { + mEmptyView.setAlpha(1.0f); transitionTo(mEmptyScene); } else { TransitionManager.go(mEmptyScene, null); + // When transition is null, transition got stuck without calling endTransitions. + TransitionManager.endTransitions(mEmptyScene.getSceneRoot()); + // Since Fade.OUT transition doesn't run, we need to set alpha manually. + mEmptyView.setAlpha(0); } } public void goToChannelBannerScene() { initIfNeeded(); - if (mMainActivity.getCurrentChannel().isPassthrough()) { + Channel channel = mMainActivity.getCurrentChannel(); + if (channel != null && channel.isPassthrough()) { if (mCurrentScene != mInputBannerScene) { // Show the input banner instead. LayoutParams lp = (LayoutParams) mInputBannerView.getLayoutParams(); @@ -152,7 +159,7 @@ public class TvTransitionManager extends TransitionManager { mExitAnimator = AnimatorInflater.loadAnimator(mMainActivity, R.animator.channel_banner_exit); - mEmptyScene = new Scene(mSceneContainer, mEmptyView); + mEmptyScene = new Scene(mSceneContainer, (View) mEmptyView); mEmptyScene.setEnterAction(new Runnable() { @Override public void run() { diff --git a/src/com/android/tv/ui/TvViewUiManager.java b/src/com/android/tv/ui/TvViewUiManager.java index d767906b..5ad89bfa 100644 --- a/src/com/android/tv/ui/TvViewUiManager.java +++ b/src/com/android/tv/ui/TvViewUiManager.java @@ -59,6 +59,7 @@ public class TvViewUiManager { private static final boolean DEBUG = false; private static final float DISPLAY_MODE_EPSILON = 0.001f; + private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f; private final Context mContext; private final Resources mResources; @@ -71,8 +72,8 @@ public class TvViewUiManager { private final int mTvViewShrunkenEndMargin; private final int mTvViewPapStartMargin; private final int mTvViewPapEndMargin; - private final int mScreenWidth; - private final int mScreenHeight; + private int mWindowWidth; + private int mWindowHeight; private final int mPipViewHorizontalMargin; private final int mPipViewTopMargin; private final int mPipViewBottomMargin; @@ -101,28 +102,28 @@ public class TvViewUiManager { private ObjectAnimator mBackgroundAnimator; private int mBackgroundColor; private int mAppliedDisplayedMode = DisplayMode.MODE_NOT_DEFINED; - private int mAppliedVideoWidth; - private int mAppliedVideoHeight; private int mAppliedTvViewStartMargin; private int mAppliedTvViewEndMargin; + private float mAppliedVideoDisplayAspectRatio; public TvViewUiManager(Context context, TunableTvView tvView, TunableTvView pipView, FrameLayout contentView, TvOptionsManager tvOptionManager) { mContext = context; - mResources = context.getResources(); + mResources = mContext.getResources(); mTvView = tvView; mPipView = pipView; mContentView = contentView; mTvOptionsManager = tvOptionManager; - DisplayManager displayManager = (DisplayManager) context + DisplayManager displayManager = (DisplayManager) mContext .getSystemService(Context.DISPLAY_SERVICE); Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); Point size = new Point(); display.getSize(size); - mScreenWidth = size.x; - mScreenHeight = size.y; + mWindowWidth = size.x; + mWindowHeight = size.y; + // Have an assumption that PIP and TvView Shrinking happens only in full screen. mTvViewShrunkenStartMargin = mResources .getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_start); mTvViewShrunkenEndMargin = @@ -131,7 +132,7 @@ public class TvViewUiManager { int papMarginHorizontal = mResources .getDimensionPixelOffset(R.dimen.papview_margin_horizontal); int papSpacing = mResources.getDimensionPixelOffset(R.dimen.papview_spacing); - mTvViewPapWidth = (mScreenWidth - papSpacing) / 2 - papMarginHorizontal; + mTvViewPapWidth = (mWindowWidth - papSpacing) / 2 - papMarginHorizontal; mTvViewPapStartMargin = papMarginHorizontal + mTvViewPapWidth + papSpacing; mTvViewPapEndMargin = papMarginHorizontal; mTvViewFrame = createMarginLayoutParams(0, 0, 0, 0); @@ -147,6 +148,21 @@ public class TvViewUiManager { .getDimensionPixelOffset(R.dimen.pipview_margin_horizontal); mPipViewTopMargin = mResources.getDimensionPixelOffset(R.dimen.pipview_margin_top); mPipViewBottomMargin = mResources.getDimensionPixelOffset(R.dimen.pipview_margin_bottom); + mContentView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + int windowWidth = right - left; + int windowHeight = bottom - top; + if (windowWidth > 0 && windowHeight > 0) { + if (mWindowWidth != windowWidth || mWindowHeight != windowHeight) { + mWindowWidth = windowWidth; + mWindowHeight = windowHeight; + applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), false, true); + } + } + } + }); } /** @@ -170,7 +186,7 @@ public class TvViewUiManager { mTvViewEndMarginBeforeShrunken = mTvViewEndMargin; if (mPipStarted && getPipLayout() == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { float sidePanelWidth = mResources.getDimensionPixelOffset(R.dimen.side_panel_width); - float factor = 1.0f - sidePanelWidth / mScreenWidth; + float factor = 1.0f - sidePanelWidth / mWindowWidth; int startMargin = (int) (mTvViewPapStartMargin * factor); int endMargin = (int) (mTvViewPapEndMargin * factor + sidePanelWidth); setTvViewMargin(startMargin, endMargin); @@ -209,25 +225,21 @@ public class TvViewUiManager { int viewWidth = mContentView.getWidth(); int viewHeight = mContentView.getHeight(); - int videoWidth = mTvView.getVideoWidth(); - int videoHeight = mTvView.getVideoHeight(); - - if (viewWidth <= 0 || viewHeight <= 0 || videoWidth <= 0 || videoHeight <= 0) { + float videoDisplayAspectRatio = mTvView.getVideoDisplayAspectRatio(); + if (viewWidth <= 0 || viewHeight <= 0 || videoDisplayAspectRatio <= 0f) { Log.w(TAG, "Video size is currently unavailable"); if (DEBUG) { Log.d(TAG, "isDisplayModeAvailable: " + "viewWidth=" + viewWidth + ", viewHeight=" + viewHeight - + ", videoWidth=" + videoWidth - + ", videoHeight="+ videoHeight + + ", videoDisplayAspectRatio=" + videoDisplayAspectRatio ); } return false; } float viewRatio = viewWidth / (float) viewHeight; - float videoRatio = videoWidth / (float) videoHeight; - return Math.abs(viewRatio - videoRatio) >= DISPLAY_MODE_EPSILON; + return Math.abs(viewRatio - videoDisplayAspectRatio) >= DISPLAY_MODE_EPSILON; } /** @@ -251,7 +263,7 @@ public class TvViewUiManager { if (storeInPreference) { mSharedPreferences.edit().putInt(TvSettings.PREF_DISPLAY_MODE, displayMode).apply(); } - applyDisplayMode(mTvView.getVideoWidth(), mTvView.getVideoHeight(), animate); + applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), animate, false); return prev; } @@ -268,7 +280,7 @@ public class TvViewUiManager { * Updates TvView. It is called when video resolution is updated. */ public void updateTvView() { - applyDisplayMode(mTvView.getVideoWidth(), mTvView.getVideoHeight(), false); + applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), false, false); if (mTvView.isVideoAvailable() && mTvView.isFadedOut()) { mTvView.fadeIn(mResources.getInteger(R.integer.tvview_fade_in_duration), mFastOutLinearIn, null); @@ -507,11 +519,11 @@ public class TvViewUiManager { @Override public void run() { if (DEBUG) { - Log.d(TAG, "setFixedSize: w=" + mTvView.getWidth() + " h=" + mTvView - .getHeight()); + Log.d(TAG, "setFixedSize: w=" + layoutParams.width + " h=" + + layoutParams.height); } mTvView.setLayoutParams(layoutParams); - mTvView.setFixedSurfaceSize(mTvView.getWidth(), mTvView.getHeight()); + mTvView.setFixedSurfaceSize(layoutParams.width, layoutParams.height); } }); } else { @@ -541,15 +553,14 @@ public class TvViewUiManager { if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { gravity = Gravity.CENTER_VERTICAL | Gravity.START; height = tvViewFrame.height; - int pipVideoWidth = mPipView.getVideoWidth(); - int pipVideoHeight = mPipView.getVideoHeight(); - if (pipVideoWidth > 0 && pipVideoHeight > 0) { - width = height * pipVideoWidth / pipVideoHeight; + float videoDisplayAspectRatio = mPipView.getVideoDisplayAspectRatio(); + if (videoDisplayAspectRatio <= 0f) { + width = tvViewFrame.width; + } else { + width = (int) (height * videoDisplayAspectRatio); if (width > tvViewFrame.width) { width = tvViewFrame.width; } - } else { - width = tvViewFrame.width; } startMargin = mResources.getDimensionPixelOffset(R.dimen.papview_margin_horizontal) * tvViewFrame.width / mTvViewPapWidth + (tvViewFrame.width - width) / 2; @@ -563,8 +574,8 @@ public class TvViewUiManager { int tvEndMargin = tvViewFrame.getMarginEnd(); int tvTopMargin = tvViewFrame.topMargin; int tvBottomMargin = tvViewFrame.bottomMargin; - float horizontalScaleFactor = (float) tvViewWidth / mScreenWidth; - float verticalScaleFactor = (float) tvViewHeight / mScreenHeight; + float horizontalScaleFactor = (float) tvViewWidth / mWindowWidth; + float verticalScaleFactor = (float) tvViewHeight / mWindowHeight; int maxWidth; if (mPipSize == TvSettings.PIP_SIZE_SMALL) { @@ -580,15 +591,14 @@ public class TvViewUiManager { } else { throw new IllegalArgumentException("Invalid PIP size: " + mPipSize); } - int pipVideoWidth = mPipView.getVideoWidth(); - int pipVideoHeight = mPipView.getVideoHeight(); - if (pipVideoWidth > 0 && pipVideoHeight > 0) { - width = height * pipVideoWidth / pipVideoHeight; + float videoDisplayAspectRatio = mPipView.getVideoDisplayAspectRatio(); + if (videoDisplayAspectRatio <= 0f) { + width = maxWidth; + } else { + width = (int) (height * videoDisplayAspectRatio); if (width > maxWidth) { width = maxWidth; } - } else { - width = maxWidth; } startMargin = tvStartMargin + (int) (mPipViewHorizontalMargin * horizontalScaleFactor); @@ -703,31 +713,29 @@ public class TvViewUiManager { }); } - private void applyDisplayMode(int videoWidth, int videoHeight, boolean animate) { + private void applyDisplayMode(float videoDisplayAspectRatio, boolean animate, + boolean forceUpdate) { if (mAppliedDisplayedMode == mDisplayMode - && mAppliedVideoWidth == videoWidth - && mAppliedVideoHeight == videoHeight && mAppliedTvViewStartMargin == mTvViewStartMargin - && mAppliedTvViewEndMargin == mTvViewEndMargin) { - return; + && mAppliedTvViewEndMargin == mTvViewEndMargin + && Math.abs(mAppliedVideoDisplayAspectRatio - videoDisplayAspectRatio) < + DISPLAY_ASPECT_RATIO_EPSILON) { + if (!forceUpdate) { + return; + } } else { mAppliedDisplayedMode = mDisplayMode; - mAppliedVideoHeight = videoHeight; - mAppliedVideoWidth = videoWidth; mAppliedTvViewStartMargin = mTvViewStartMargin; mAppliedTvViewEndMargin = mTvViewEndMargin; + mAppliedVideoDisplayAspectRatio = videoDisplayAspectRatio; } - int availableAreaWidth = mScreenWidth - mTvViewStartMargin - mTvViewEndMargin; - int availableAreaHeight = availableAreaWidth * mScreenHeight / mScreenWidth; + int availableAreaWidth = mWindowWidth - mTvViewStartMargin - mTvViewEndMargin; + int availableAreaHeight = availableAreaWidth * mWindowHeight / mWindowWidth; FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(0, 0, ((FrameLayout.LayoutParams) mTvView.getLayoutParams()).gravity); int displayMode = mDisplayMode; double availableAreaRatio = 0; double videoRatio = 0; - if (videoWidth <= 0 || videoHeight <= 0) { - videoWidth = mScreenWidth; - videoHeight = mScreenHeight; - } if (availableAreaWidth <= 0 || availableAreaHeight <= 0) { displayMode = DisplayMode.MODE_FULL; Log.w(TAG, "Some resolution info is missing during applyDisplayMode. (" @@ -735,10 +743,14 @@ public class TvViewUiManager { + availableAreaHeight + ")"); } else { availableAreaRatio = (double) availableAreaWidth / availableAreaHeight; - videoRatio = (double) videoWidth / videoHeight; + if (videoDisplayAspectRatio <= 0f) { + videoRatio = (double) mWindowWidth / mWindowHeight; + } else { + videoRatio = videoDisplayAspectRatio; + } } - int tvViewFrameTop = (mScreenHeight - availableAreaHeight) / 2; + int tvViewFrameTop = (mWindowHeight - availableAreaHeight) / 2; MarginLayoutParams tvViewFrame = createMarginLayoutParams( mTvViewStartMargin, mTvViewEndMargin, tvViewFrameTop, tvViewFrameTop); layoutParams.width = availableAreaWidth; @@ -777,7 +789,7 @@ public class TvViewUiManager { int marginStart = mTvViewStartMargin + (availableAreaWidth - layoutParams.width) / 2; layoutParams.setMarginStart(marginStart); // Set marginEnd as well because setTvViewPosition uses both start/end margin. - layoutParams.setMarginEnd(mScreenWidth - layoutParams.width - marginStart); + layoutParams.setMarginEnd(mWindowWidth - layoutParams.width - marginStart); setBackgroundColor(Utils.getColor(mResources, isTvViewFullScreen() ? R.color.tvactivity_background : R.color.tvactivity_background_on_shrunken_tvview), @@ -810,8 +822,8 @@ public class TvViewUiManager { lp.setMarginEnd(endMargin); lp.topMargin = topMargin; lp.bottomMargin = bottomMargin; - lp.width = mScreenWidth - startMargin - endMargin; - lp.height = mScreenHeight - topMargin - bottomMargin; + lp.width = mWindowWidth - startMargin - endMargin; + lp.height = mWindowHeight - topMargin - bottomMargin; return lp; } } diff --git a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java index 85050dc4..b52302b6 100644 --- a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java +++ b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java @@ -37,6 +37,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Iterator; public class CustomizeChannelListFragment extends SideFragment { private static final int GROUP_BY_SOURCE = 0; @@ -157,6 +158,21 @@ public class CustomizeChannelListFragment extends SideFragment { return mItems; } + private void cleanUpOneChannelGroupItem(List<Item> items) { + Iterator<Item> iter = items.iterator(); + while (iter.hasNext()) { + Item item = iter.next(); + if (item instanceof SelectGroupItem) { + SelectGroupItem selectGroupItem = (SelectGroupItem) item; + if (selectGroupItem.mChannelItemsInGroup.size() == 1) { + ((ChannelItem) selectGroupItem.mChannelItemsInGroup.get(0)) + .mSelectGroupItem = null; + iter.remove(); + } + } + } + } + private void addItemForGroupBySource(List<Item> items) { items.add(new GroupBySubMenu(getString(R.string.edit_channels_group_by_sources))); SelectGroupItem selectGroupItem = null; @@ -177,6 +193,7 @@ public class CustomizeChannelListFragment extends SideFragment { items.add(channelItem); selectGroupItem.addChannelItem(channelItem); } + cleanUpOneChannelGroupItem(items); } private void addItemForGroupByHdSd(List<Item> items) { @@ -211,6 +228,7 @@ public class CustomizeChannelListFragment extends SideFragment { items.add(channelItem); selectGroupItem.addChannelItem(channelItem); } + cleanUpOneChannelGroupItem(items); } private static boolean isHdChannel(Channel channel) { @@ -275,7 +293,7 @@ public class CustomizeChannelListFragment extends SideFragment { } private class ChannelItem extends ChannelCheckItem { - private final SelectGroupItem mSelectGroupItem; + private SelectGroupItem mSelectGroupItem; public ChannelItem(Channel channel, SelectGroupItem selectGroupItem) { super(channel, getChannelDataManager(), getProgramDataManager()); @@ -292,7 +310,9 @@ public class CustomizeChannelListFragment extends SideFragment { protected void onSelected() { super.onSelected(); getChannelDataManager().updateBrowsable(getChannel().getId(), isChecked()); - mSelectGroupItem.notifyUpdated(); + if (mSelectGroupItem != null) { + mSelectGroupItem.notifyUpdated(); + } } @Override diff --git a/src/com/android/tv/ui/sidepanel/DeveloperFragment.java b/src/com/android/tv/ui/sidepanel/DeveloperFragment.java deleted file mode 100644 index 44b4d452..00000000 --- a/src/com/android/tv/ui/sidepanel/DeveloperFragment.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.ui.sidepanel; - -import android.app.Activity; -import android.content.Context; -import android.view.View; - -import com.android.tv.Features; -import com.android.tv.R; - -import java.util.ArrayList; -import java.util.List; - -/** - * Shows developer options like enabling USB TV tuner. - */ -public class DeveloperFragment extends SideFragment { - private static final String TRACKER_LABEL = "developer options"; - - /** - * Sets USB TV tuner enabled. - */ - private static final class UsbTvTunerItem extends SwitchItem { - Context mContext; - - public UsbTvTunerItem(Context context) { - super(context.getResources().getString(R.string.developer_menu_enable_usb_tv_tuner), - context.getResources().getString(R.string.developer_menu_enable_usb_tv_tuner), - context.getResources().getString( - R.string.developer_menu_enable_usb_tv_tuner_description)); - mContext = context; - } - - @Override - protected void onBind(View view) { - super.onBind(view); - setChecked(Features.USB_TUNER.isEnabled(view.getContext())); - } - - @Override - public void setChecked(boolean checked) { - super.setChecked(checked); - Features.USB_TUNER.setEnabled(mContext, checked); - } - } - - @Override - protected String getTitle() { - return getResources().getString(R.string.side_panel_title_developer); - } - - @Override - public String getTrackerLabel() { - return TRACKER_LABEL; - } - - @Override - protected List<Item> getItemList() { - List<Item> items = new ArrayList<>(); - Activity activity = getActivity(); - items.add(new UsbTvTunerItem(activity)); - boolean ac3Support = getMainActivity().isAc3PassthroughSupported(); - // Show AC3 passthrough availability. - items.add(new SimpleItem(getString(R.string.developer_menu_ac3_support), - getString(ac3Support ? R.string.developer_menu_ac3_support_yes - : R.string.developer_menu_ac3_support_no))); - return items; - } -} diff --git a/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java b/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java index 06415c21..dec017a8 100644 --- a/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java +++ b/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java @@ -156,8 +156,11 @@ public class PipInputSelectorFragment extends SideFragment { // If this input shares the same parent with the current main input, you cannot select // it. (E.g. two HDMI CEC devices that are connected to HDMI port 1 through an A/V // receiver.) - TvInputInfo mainInputInfo = mPipInputManager.getPipInput( - getMainActivity().getCurrentChannel()).getInputInfo(); + PipInput pipInput = mPipInputManager.getPipInput(getMainActivity().getCurrentChannel()); + if (pipInput == null) { + return false; + } + TvInputInfo mainInputInfo = pipInput.getInputInfo(); TvInputInfo pipInputInfo = mPipInput.getInputInfo(); return mainInputInfo == null || pipInputInfo == null || !TextUtils.equals(mainInputInfo.getId(), pipInputInfo.getId()) diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java index 6b5b2584..6d606014 100644 --- a/src/com/android/tv/ui/sidepanel/SettingsFragment.java +++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java @@ -16,13 +16,9 @@ package com.android.tv.ui.sidepanel; -import android.content.res.Resources; -import android.os.Build; -import android.provider.Settings; import android.view.View; import android.widget.Toast; -import com.android.tv.Features; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvApplication; @@ -155,19 +151,6 @@ public class SettingsFragment extends SideFragment { if (LicenseUtils.hasLicenses(activity.getAssets())) { items.add(new LicenseActionItem(activity)); } - boolean developerOptionEnabled = Settings.Secure.getInt(getActivity().getContentResolver(), - Settings.Global.DEVELOPMENT_SETTINGS_ENABLED , 0) != 0; - if (Features.DEVELOPER_OPTION.isEnabled(getActivity()) && developerOptionEnabled - && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - Resources res = getActivity().getResources(); - items.add(new ActionItem(res.getString(R.string.side_panel_title_developer)) { - @Override - protected void onSelected() { - getMainActivity().getOverlayManager().getSideFragmentManager().show( - new DeveloperFragment()); - } - }); - } // Show version. items.add(new SimpleItem(getString(R.string.settings_menu_version), ((TvApplication) activity.getApplicationContext()).getVersionName())); diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java index 7ec28bb8..6bc47939 100644 --- a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java +++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java @@ -18,6 +18,7 @@ package com.android.tv.ui.sidepanel.parentalcontrols; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.util.ArrayMap; import android.util.SparseIntArray; import android.view.View; import android.widget.CompoundButton; @@ -41,6 +42,7 @@ import com.android.tv.util.TvSettings.ContentRatingLevel; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; public class RatingsFragment extends SideFragment { private static final SparseIntArray sLevelResourceIdMap; @@ -73,6 +75,9 @@ public class RatingsFragment extends SideFragment { } private final List<RatingLevelItem> mRatingLevelItems = new ArrayList<>(); + // A map from the rating system ID string to RatingItem objects. + private final Map<String, List<RatingItem>> mContentRatingSystemItemMap = new ArrayMap<>(); + private ParentalControlSettings mParentalControlSettings; public static String getDescription(MainActivity tvActivity) { @ContentRatingLevel int currentLevel = @@ -104,18 +109,32 @@ public class RatingsFragment extends SideFragment { updateRatingLevels(); items.addAll(mRatingLevelItems); + mContentRatingSystemItemMap.clear(); + List<ContentRatingSystem> contentRatingSystems = getMainActivity().getContentRatingsManager().getContentRatingSystems(); Collections.sort(contentRatingSystems, ContentRatingSystem.DISPLAY_NAME_COMPARATOR); for (ContentRatingSystem s : contentRatingSystems) { - if (getMainActivity().getParentalControlSettings().isContentRatingSystemEnabled(s)) { + if (mParentalControlSettings.isContentRatingSystemEnabled(s)) { + List<RatingItem> ratingItems = new ArrayList<>(); + boolean hasSubRating = false; items.add(new DividerItem(s.getDisplayName())); for (Rating rating : s.getRatings()) { - RatingItem item = rating.getSubRatings().size() == 0 ? + RatingItem item = rating.getSubRatings().isEmpty() ? new RatingItem(s, rating) : new RatingWithSubItem(s, rating); items.add(item); + if (rating.getSubRatings().isEmpty()) { + ratingItems.add(item); + } else { + hasSubRating = true; + } + } + // Only include rating systems that don't contain any sub ratings in the map for + // simplicity. + if (!hasSubRating) { + mContentRatingSystemItemMap.put(s.getId(), ratingItems); } } } @@ -131,7 +150,8 @@ public class RatingsFragment extends SideFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - getMainActivity().getParentalControlSettings().loadRatings(); + mParentalControlSettings = getMainActivity().getParentalControlSettings(); + mParentalControlSettings.loadRatings(); } @Override @@ -146,13 +166,27 @@ public class RatingsFragment extends SideFragment { } private void updateRatingLevels() { - @ContentRatingLevel int ratingLevel = - getMainActivity().getParentalControlSettings().getContentRatingLevel(); + @ContentRatingLevel int ratingLevel = mParentalControlSettings.getContentRatingLevel(); for (RatingLevelItem ratingLevelItem : mRatingLevelItems) { ratingLevelItem.setChecked(ratingLevel == ratingLevelItem.mRatingLevel); } } + private void updateDependentRatingItems(ContentRatingSystem.Order order, + int selectedRatingOrderIndex, String contentRatingSystemId, boolean isChecked) { + List<RatingItem> ratingItems = mContentRatingSystemItemMap.get(contentRatingSystemId); + if (ratingItems != null) { + for (RatingItem item : ratingItems) { + int ratingOrderIndex = item.getRatingOrderIndex(order); + if (ratingOrderIndex != -1 + && ((ratingOrderIndex > selectedRatingOrderIndex && isChecked) + || (ratingOrderIndex < selectedRatingOrderIndex && !isChecked))) { + item.setRatingBlocked(isChecked); + } + } + } + } + private class RatingLevelItem extends RadioButtonItem { private final int mRatingLevel; @@ -166,7 +200,7 @@ public class RatingsFragment extends SideFragment { @Override protected void onSelected() { super.onSelected(); - getMainActivity().getParentalControlSettings().setContentRatingLevel( + mParentalControlSettings.setContentRatingLevel( getMainActivity().getContentRatingsManager(), mRatingLevel); notifyItemsChanged(mRatingLevelItems.size()); } @@ -177,12 +211,21 @@ public class RatingsFragment extends SideFragment { protected final Rating mRating; private final Drawable mIcon; private CompoundButton mCompoundButton; + private final List<ContentRatingSystem.Order> mOrders = new ArrayList<>(); + private final List<Integer> mOrderIndexes = new ArrayList<>(); private RatingItem(ContentRatingSystem contentRatingSystem, Rating rating) { super(rating.getTitle(), rating.getDescription()); mContentRatingSystem = contentRatingSystem; mRating = rating; mIcon = rating.getIcon(); + for (ContentRatingSystem.Order order : mContentRatingSystem.getOrders()) { + int orderIndex = order.getRatingIndex(mRating); + if (orderIndex != -1) { + mOrders.add(order); + mOrderIndexes.add(orderIndex); + } + } } @Override @@ -211,17 +254,21 @@ public class RatingsFragment extends SideFragment { protected void onUpdate() { super.onUpdate(); mCompoundButton.setButtonDrawable(getButtonDrawable()); - setChecked(getMainActivity().getParentalControlSettings().isRatingBlocked( - mContentRatingSystem, mRating)); + setChecked(mParentalControlSettings.isRatingBlocked(mContentRatingSystem, mRating)); } @Override protected void onSelected() { super.onSelected(); - if (getMainActivity().getParentalControlSettings() - .setRatingBlocked(mContentRatingSystem, mRating, isChecked())) { + if (mParentalControlSettings.setRatingBlocked( + mContentRatingSystem, mRating, isChecked())) { updateRatingLevels(); } + // Automatically check/uncheck dependent ratings. + for (int i = 0; i < mOrders.size(); i++) { + updateDependentRatingItems(mOrders.get(i), mOrderIndexes.get(i), + mContentRatingSystem.getId(), isChecked()); + } } @Override @@ -232,6 +279,19 @@ public class RatingsFragment extends SideFragment { protected int getButtonDrawable() { return R.drawable.btn_lock_material_anim; } + + private int getRatingOrderIndex(ContentRatingSystem.Order order) { + int orderIndex = mOrders.indexOf(order); + return orderIndex == -1 ? -1 : mOrderIndexes.get(orderIndex); + } + + private void setRatingBlocked(boolean isChecked) { + if (isChecked() == isChecked) { + return; + } + mParentalControlSettings.setRatingBlocked(mContentRatingSystem, mRating, isChecked); + notifyUpdated(); + } } private class RatingWithSubItem extends RatingItem { @@ -247,7 +307,7 @@ public class RatingsFragment extends SideFragment { @Override protected int getButtonDrawable() { - int blockedStatus = getMainActivity().getParentalControlSettings().getBlockedStatus( + int blockedStatus = mParentalControlSettings.getBlockedStatus( mContentRatingSystem, mRating); if (blockedStatus == ParentalControlSettings.RATING_BLOCKED) { return R.drawable.btn_lock_material; diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java index 9f440533..7ac293fc 100644 --- a/src/com/android/tv/util/AsyncDbTask.java +++ b/src/com/android/tv/util/AsyncDbTask.java @@ -22,10 +22,12 @@ import android.media.tv.TvContract; import android.net.Uri; import android.os.AsyncTask; import android.support.annotation.MainThread; +import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import android.util.Log; import android.util.Range; +import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.Program; @@ -40,6 +42,10 @@ import java.util.concurrent.RejectedExecutionException; * * <p>Instances of this class should only be executed this using {@link * #executeOnDbThread(Object[])}. + * + * @param <Params> the type of the parameters sent to the task upon execution. + * @param <Progress> the type of the progress units published during the background computation. + * @param <Result> the type of the result of the background computation. */ public abstract class AsyncDbTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> { @@ -79,7 +85,7 @@ public abstract class AsyncDbTask<Params, Progress, Result> * <p> {@link #doInBackground(Void...)} executes the query on call {@link #onQuery(Cursor)} * which is implemented by subclasses. * - * @param <Result> The type of result returned by {@link #onQuery(Cursor)} + * @param <Result> the type of result returned by {@link #onQuery(Cursor)} */ public abstract static class AsyncQueryTask<Result> extends AsyncDbTask<Void, Void, Result> { private final ContentResolver mContentResolver; @@ -103,9 +109,10 @@ public abstract class AsyncDbTask<Params, Progress, Result> @Override protected final Result doInBackground(Void... params) { if (!THREAD_FACTORY.namedWithPrefix(Thread.currentThread())) { - IllegalStateException e = new IllegalStateException( - this + " should only be executed using executeOnDbThread, " + - "but it was called on thread " + Thread.currentThread()); + IllegalStateException e = new IllegalStateException(this + + " should only be executed using executeOnDbThread, " + + "but it was called on thread " + + Thread.currentThread()); Log.w(TAG, e); if (DEBUG) { throw e; @@ -137,8 +144,8 @@ public abstract class AsyncDbTask<Params, Progress, Result> } return null; } - } catch (SecurityException e) { - Log.d(TAG, "Security exception during query", e); + } catch (Exception e) { + SoftPreconditions.warn(TAG, null, "Error querying " + this, e); return null; } } @@ -161,8 +168,10 @@ public abstract class AsyncDbTask<Params, Progress, Result> * Returns the result of a query as an {@link List} of {@code T}. * * <p>Subclasses must implement {@link #fromCursor(Cursor)}. + * + * @param <T> the type of result returned in a list by {@link #onQuery(Cursor)} */ - public static abstract class AsyncQueryListTask<T> extends AsyncQueryTask<List<T>> { + public abstract static class AsyncQueryListTask<T> extends AsyncQueryTask<List<T>> { public AsyncQueryListTask(ContentResolver contentResolver, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy) { @@ -201,9 +210,56 @@ public abstract class AsyncDbTask<Params, Progress, Result> } /** + * Returns the result of a query as a single instance of {@code T}. + * + * <p>Subclasses must implement {@link #fromCursor(Cursor)}. + */ + public abstract static class AsyncQueryItemTask<T> extends AsyncQueryTask<T> { + + public AsyncQueryItemTask(ContentResolver contentResolver, Uri uri, String[] projection, + String selection, String[] selectionArgs, String orderBy) { + super(contentResolver, uri, projection, selection, selectionArgs, orderBy); + } + + @Override + protected final T onQuery(Cursor c) { + if (c.moveToNext()) { + if (isCancelled()) { + // This is guaranteed to never call onPostExecute because the task is canceled. + return null; + } + T result = fromCursor(c); + if (c.moveToNext()) { + Log.w(TAG, "More than one result for found for " + this); + } + return result; + } else { + if (DEBUG) { + Log.v(TAG, "No result for found for " + this); + } + return null; + } + + } + + /** + * Return a single instance of {@code T} from the cursor. + * + * <p><b>NOTE</b> Do not move the cursor or close it, that is handled by {@link + * #onQuery(Cursor)}. + * + * <p><b>Note</b> This is executed on the DB thread by {@link #onQuery(Cursor)} + * + * @param c The cursor with the values to create T from. + */ + @WorkerThread + protected abstract T fromCursor(Cursor c); + } + + /** * Gets an {@link List} of {@link Channel}s from {@link TvContract.Channels#CONTENT_URI}. */ - public static abstract class AsyncChannelQueryTask extends AsyncQueryListTask<Channel> { + public abstract static class AsyncChannelQueryTask extends AsyncQueryListTask<Channel> { public AsyncChannelQueryTask(ContentResolver contentResolver) { super(contentResolver, TvContract.Channels.CONTENT_URI, Channel.PROJECTION, @@ -227,16 +283,19 @@ public abstract class AsyncDbTask<Params, Progress, Result> /** * Gets an {@link List} of {@link Program}s for a given channel and period {@link - * TvContract#buildProgramsUriForChannel(long, long, long)}. + * TvContract#buildProgramsUriForChannel(long, long, long)}. If the {@code period} is + * {@code null}, then all the programs is queried. */ public static class LoadProgramsForChannelTask extends AsyncQueryListTask<Program> { protected final Range<Long> mPeriod; protected final long mChannelId; public LoadProgramsForChannelTask(ContentResolver contentResolver, long channelId, - Range<Long> period) { - super(contentResolver, TvContract - .buildProgramsUriForChannel(channelId, period.getLower(), period.getUpper()), + @Nullable Range<Long> period) { + super(contentResolver, period == null + ? TvContract.buildProgramsUriForChannel(channelId) + : TvContract.buildProgramsUriForChannel(channelId, period.getLower(), + period.getUpper()), Program.PROJECTION, null, null, null); mPeriod = period; mChannelId = channelId; diff --git a/src/com/android/tv/util/ImageLoader.java b/src/com/android/tv/util/ImageLoader.java index 59c4983b..ed0fd54d 100644 --- a/src/com/android/tv/util/ImageLoader.java +++ b/src/com/android/tv/util/ImageLoader.java @@ -28,18 +28,23 @@ import android.support.annotation.MainThread; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.support.annotation.WorkerThread; +import android.util.ArraySet; import android.util.Log; import com.android.tv.R; -import com.android.tv.common.CollectionUtils; import com.android.tv.util.BitmapUtils.ScaledBitmapInfo; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; /** * This class wraps up completing some arbitrary long running work when loading a bitmap. It @@ -49,6 +54,39 @@ public final class ImageLoader { private static final String TAG = "ImageLoader"; private static final boolean DEBUG = false; + private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); + // We want at least 2 threads and at most 4 threads in the core pool, + // preferring to have 1 less than the CPU count to avoid saturating + // the CPU with background work + private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4)); + private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; + private static final int KEEP_ALIVE_SECONDS = 30; + + private static final ThreadFactory sThreadFactory = new NamedThreadFactory("ImageLoader"); + + private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<Runnable>( + 128); + + /** + * An private {@link Executor} that can be used to execute tasks in parallel. + * + * <p>{@code IMAGE_THREAD_POOL_EXECUTOR} setting are copied from {@link AsyncTask} + * Since we do a lot of concurrent image loading we can exhaust a thread pool. + * ImageLoader catches the error, and just leaves the image blank. + * However other tasks will fail and crash the application. + * + * <p>Using a separate thread pool prevents image loading from causing other tasks to fail. + */ + private static final Executor IMAGE_THREAD_POOL_EXECUTOR; + + static { + ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, + MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, sPoolWorkQueue, + sThreadFactory); + threadPoolExecutor.allowCoreThreadTimeOut(true); + IMAGE_THREAD_POOL_EXECUTOR = threadPoolExecutor; + } + private static Handler sMainHandler; /** @@ -148,7 +186,7 @@ public final class ImageLoader { Log.d(TAG, "loadBitmap() " + uriString); } return doLoadBitmap(context, uriString, maxWidth, maxHeight, callback, - AsyncTask.THREAD_POOL_EXECUTOR); + IMAGE_THREAD_POOL_EXECUTOR); } private static boolean doLoadBitmap(Context context, String uriString, @@ -179,7 +217,7 @@ public final class ImageLoader { if (DEBUG) { Log.d(TAG, "loadBitmap() " + loadBitmapTask); } - return doLoadBitmap(callback, AsyncTask.THREAD_POOL_EXECUTOR, loadBitmapTask); + return doLoadBitmap(callback, IMAGE_THREAD_POOL_EXECUTOR, loadBitmapTask); } /** @@ -226,7 +264,7 @@ public final class ImageLoader { protected final Context mAppContext; protected final int mMaxWidth; protected final int mMaxHeight; - private final Set<ImageLoaderCallback> mCallbacks = CollectionUtils.createSmallSet(); + private final Set<ImageLoaderCallback> mCallbacks = new ArraySet<>(); private final ImageCache mImageCache; private final String mKey; diff --git a/src/com/android/tv/util/MultiLongSparseArray.java b/src/com/android/tv/util/MultiLongSparseArray.java index 7ed72d61..1d5fa80b 100644 --- a/src/com/android/tv/util/MultiLongSparseArray.java +++ b/src/com/android/tv/util/MultiLongSparseArray.java @@ -17,10 +17,9 @@ package com.android.tv.util; import android.support.annotation.VisibleForTesting; +import android.util.ArraySet; import android.util.LongSparseArray; -import com.android.tv.common.CollectionUtils; - import java.util.Collections; import java.util.Set; @@ -105,7 +104,7 @@ public class MultiLongSparseArray<T> { private Set<T> getEmptySet() { if (mEmptyIndex < 0) { - return CollectionUtils.createSmallSet(); + return new ArraySet<>(); } Set<T> emptySet = mEmptySets[mEmptyIndex]; mEmptySets[mEmptyIndex--] = null; diff --git a/src/com/android/tv/util/NetworkUtils.java b/src/com/android/tv/util/NetworkUtils.java new file mode 100644 index 00000000..ed3ce383 --- /dev/null +++ b/src/com/android/tv/util/NetworkUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.util; + +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * A utility class to check the connectivity. + */ +@WorkerThread +public class NetworkUtils { + private static final String GENERATE_204 = "http://clients3.google.com/generate_204"; + + /** + * Checks if the internet connection is available. + */ + public static boolean isNetworkAvailable(@Nullable ConnectivityManager connectivityManager) { + if (connectivityManager == null) { + return false; + } + NetworkInfo info = connectivityManager.getActiveNetworkInfo(); + if (info == null || !info.isConnected()) { + return false; + } + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) new URL(GENERATE_204).openConnection(); + connection.setInstanceFollowRedirects(false); + connection.setDefaultUseCaches(false); + connection.setUseCaches(false); + if (connection.getResponseCode() == HttpURLConnection.HTTP_NO_CONTENT) { + return true; + } + } catch (IOException e) { + // Does nothing. + } finally { + if (connection != null) { + connection.disconnect(); + } + } + return false; + } + + private NetworkUtils() { } +} diff --git a/src/com/android/tv/util/OnboardingUtils.java b/src/com/android/tv/util/OnboardingUtils.java index 582a0c9f..3dcc324d 100644 --- a/src/com/android/tv/util/OnboardingUtils.java +++ b/src/com/android/tv/util/OnboardingUtils.java @@ -37,7 +37,7 @@ public final class OnboardingUtils { private static final int ONBOARDING_VERSION = 1; private static final String MERCHANT_COLLECTION_URL_STRING = - "https://play.google.com/store/apps/collection/promotion_3001bf9_ATV_livechannels"; + "TODO: put a market link to show TV input apps"; /** * Intent to show merchant collection in play store. */ @@ -101,7 +101,7 @@ public final class OnboardingUtils { ContentResolver resolver = context.getContentResolver(); try (Cursor c = resolver.query(Channels.CONTENT_URI, new String[] {Channels._ID}, null, null, null)) { - return c.getCount() != 0; + return c != null && c.getCount() != 0; } } diff --git a/src/com/android/tv/util/PipInputManager.java b/src/com/android/tv/util/PipInputManager.java index dddc82b6..03bdc681 100644 --- a/src/com/android/tv/util/PipInputManager.java +++ b/src/com/android/tv/util/PipInputManager.java @@ -20,11 +20,11 @@ import android.content.Context; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvInputManager.TvInputCallback; +import android.util.ArraySet; import android.util.Log; import com.android.tv.ChannelTuner; import com.android.tv.R; -import com.android.tv.common.CollectionUtils; import com.android.tv.data.Channel; import java.util.ArrayList; @@ -37,6 +37,7 @@ import java.util.Set; /** * A class that manages inputs for PIP. All tuner inputs are represented to one tuner input for PIP. + * Hidden inputs should not be visible to the users. */ public class PipInputManager { private static final String TAG = "PipInputManager"; @@ -50,7 +51,7 @@ public class PipInputManager { private final ChannelTuner mChannelTuner; private boolean mStarted; private final Map<String, PipInput> mPipInputMap = new HashMap<>(); // inputId -> PipInput - private final Set<Listener> mListeners = CollectionUtils.createSmallSet(); + private final Set<Listener> mListeners = new ArraySet<>(); private final TvInputCallback mTvInputCallback = new TvInputCallback() { @Override @@ -182,40 +183,53 @@ public class PipInputManager { /** * Gets the size of inputs for PIP. * - * @param availableOnly If true, it counts only available PIP inputs. Please see {@link + * <p>The hidden inputs are not counted. + * + * @param availableOnly If {@code true}, it counts only available PIP inputs. Please see {@link * PipInput#isAvailable()} for the details of availability. */ public int getPipInputSize(boolean availableOnly) { - if (availableOnly) { - int count = 0; - for (PipInput pipInput : mPipInputMap.values()) { - if (pipInput.isAvailable()) { - ++count; + int count = 0; + for (PipInput pipInput : mPipInputMap.values()) { + if (!pipInput.isHidden() && (!availableOnly || pipInput.mAvailable)) { + ++count; + } + if (pipInput.isPassthrough()) { + TvInputInfo info = pipInput.getInputInfo(); + // Do not count HDMI ports if a CEC device is directly connected to the port. + if (info.getParentId() != null && !info.isConnectedToHdmiSwitch()) { + --count; } } - return count; - } else { - return mPipInputMap.size(); } + return count; } /** - * Gets the list of inputs for PIP. + * Gets the list of inputs for PIP.. + * + * <p>The hidden inputs are excluded. * * @param availableOnly If true, it returns only available PIP inputs. Please see {@link * PipInput#isAvailable()} for the details of availability. */ public List<PipInput> getPipInputList(boolean availableOnly) { - List<PipInput> pipInputs; - if (availableOnly) { - pipInputs = new ArrayList<>(); - for (PipInput pipInput : mPipInputMap.values()) { - if (pipInput.mAvailable) { - pipInputs.add(pipInput); + List<PipInput> pipInputs = new ArrayList<>(); + List<PipInput> removeInputs = new ArrayList<>(); + for (PipInput pipInput : mPipInputMap.values()) { + if (!pipInput.isHidden() && (!availableOnly || pipInput.mAvailable)) { + pipInputs.add(pipInput); + } + if (pipInput.isPassthrough()) { + TvInputInfo info = pipInput.getInputInfo(); + // Do not show HDMI ports if a CEC device is directly connected to the port. + if (info.getParentId() != null && !info.isConnectedToHdmiSwitch()) { + removeInputs.add(mPipInputMap.get(info.getParentId())); } } - } else { - pipInputs = new ArrayList<>(mPipInputMap.values()); + } + if (!removeInputs.isEmpty()) { + pipInputs.removeAll(removeInputs); } Collections.sort(pipInputs, new Comparator<PipInput>() { @Override @@ -325,9 +339,9 @@ public class PipInputManager { } /** - * Returns true, if the input is available for PIP. If a channel of an input is already - * played or an input is not connected state or there is no browsable channel, the input - * is unavailable. + * Returns {@code true}, if the input is available for PIP. If a channel of an input is + * already played or an input is not connected state or there is no browsable channel, the + * input is unavailable. */ public boolean isAvailable() { return mAvailable; @@ -407,5 +421,10 @@ public class PipInputManager { } } } + + private boolean isHidden() { + // mInputInfo is null for the tuner input and it's always visible. + return mInputInfo != null && mInputInfo.isHidden(mContext); + } } } diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java index 2a006f9e..5e65715e 100644 --- a/src/com/android/tv/util/RecurringRunner.java +++ b/src/com/android/tv/util/RecurringRunner.java @@ -24,6 +24,7 @@ import android.support.annotation.WorkerThread; import android.util.Log; import com.android.tv.common.SharedPreferencesUtils; +import com.android.tv.common.SoftPreconditions; import java.util.Date; diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java index d337139b..6d24d5bd 100644 --- a/src/com/android/tv/util/SetupUtils.java +++ b/src/com/android/tv/util/SetupUtils.java @@ -27,11 +27,13 @@ import android.os.Build; import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.support.annotation.UiThread; +import android.text.TextUtils; +import android.util.ArraySet; import android.util.Log; import com.android.tv.ApplicationSingletons; import com.android.tv.TvApplication; -import com.android.tv.common.CollectionUtils; +import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; @@ -67,13 +69,13 @@ public class SetupUtils { private SetupUtils(TvApplication tvApplication) { mTvApplication = tvApplication; mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(tvApplication); - mSetUpInputs = CollectionUtils.createSmallSet(); + mSetUpInputs = new ArraySet<>(); mSetUpInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.<String>emptySet())); - mKnownInputs = CollectionUtils.createSmallSet(); + mKnownInputs = new ArraySet<>(); mKnownInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_KNOWN_INPUTS, Collections.<String>emptySet())); - mRecognizedInputs = CollectionUtils.createSmallSet(); + mRecognizedInputs = new ArraySet<>(); mRecognizedInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs)); mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true); @@ -262,29 +264,26 @@ public class SetupUtils { * @param context The Context used for granting permission. */ public static void grantEpgPermissionToSetUpPackages(Context context) { - // TvProvider allows granting of Uri permissions starting from MNC. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context); - Set<String> setUpInputs = new HashSet<>(sharedPreferences.getStringSet( - PREF_KEY_SET_UP_INPUTS, new HashSet<String>())); - Set<String> setUpPackages = new HashSet<>(); - for (String input : setUpInputs) { - ComponentName componentName = null; - try { - componentName = ComponentName.unflattenFromString(input); - } catch (Exception e) { - Log.w(TAG, "Failed to unflatten string to component name (" + input + ")", e); - } - if (componentName == null) { - continue; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + // Can't grant permission. + return; + } + + // Find all already-verified packages. + Set<String> setUpPackages = new HashSet<>(); + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); + for (String input : sp.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.EMPTY_SET)) { + if (!TextUtils.isEmpty(input)) { + ComponentName componentName = ComponentName.unflattenFromString(input); + if (componentName != null) { + setUpPackages.add(componentName.getPackageName()); } - setUpPackages.add(componentName.getPackageName()); - } - for (String packageName : setUpPackages) { - grantEpgPermission(context, packageName); } } + + for (String packageName : setUpPackages) { + grantEpgPermission(context, packageName); + } } /** @@ -346,7 +345,7 @@ public class SetupUtils { } mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs) .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs) - .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs).apply(); + .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs).apply(); } } @@ -360,7 +359,7 @@ public class SetupUtils { if (!mRecognizedInputs.contains(inputId)) { Log.i(TAG, "An unrecognized input's setup has been done. inputId=" + inputId); mRecognizedInputs.add(inputId); - mSharedPreferences.edit().putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs) + mSharedPreferences.edit().putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) .apply(); } if (!mKnownInputs.contains(inputId)) { diff --git a/src/com/android/tv/util/SoftPreconditions.java b/src/com/android/tv/util/SoftPreconditions.java deleted file mode 100644 index 3643fca4..00000000 --- a/src/com/android/tv/util/SoftPreconditions.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.util; - -import android.content.Context; -import android.text.TextUtils; -import android.util.Log; - -import com.android.tv.common.BuildConfig; -import com.android.tv.common.feature.Feature; - -/** - * Simple static methods to be called at the start of your own methods to verify - * correct arguments and state. - * - * <p>{@code checkXXX} methods throw exceptions when {@link BuildConfig#ENG} is true, and - * logs a warning when it is false. - * - * <p>This is based on com.android.internal.util.Preconditions. - */ -public final class SoftPreconditions { - private static final String TAG = "SoftPreconditions"; - - /** - * Throws or logs if an expression involving the parameter of the calling - * method is not true. - * - * @param expression a boolean expression - * @param tag Used to identify the source of a log message. It usually - * identifies the class or activity where the log call occurs. - * @param msg The message you would like logged. - * @throws IllegalArgumentException if {@code expression} is true - */ - public static void checkArgument(final boolean expression, String tag, String msg) { - if (!expression) { - warn(tag, "Illegal argument", msg, new IllegalArgumentException(msg)); - } - } - - /** - * Throws or logs if an expression involving the parameter of the calling - * method is not true. - * - * @param expression a boolean expression - * @throws IllegalArgumentException if {@code expression} is true - */ - public static void checkArgument(final boolean expression) { - checkArgument(expression, null, null); - } - - /** - * Throws or logs if an and object is null. - * - * @param reference an object reference - * @param tag Used to identify the source of a log message. It usually - * identifies the class or activity where the log call occurs. - * @param msg The message you would like logged. - * @return true if the object is null - * @throws NullPointerException if {@code reference} is null - */ - public static <T> T checkNotNull(final T reference, String tag, String msg) { - if (reference == null) { - warn(tag, "Null Pointer", msg, new NullPointerException(msg)); - } - return reference; - } - - /** - * Throws or logs if an and object is null. - * - * @param reference an object reference - * @return true if the object is null - * @throws NullPointerException if {@code reference} is null - */ - public static <T> T checkNotNull(final T reference) { - return checkNotNull(reference, null, null); - } - - /** - * Throws or logs if an expression involving the state of the calling - * instance, but not involving any parameters to the calling method is not true. - * - * @param expression a boolean expression - * @param tag Used to identify the source of a log message. It usually - * identifies the class or activity where the log call occurs. - * @param msg The message you would like logged. - * @throws IllegalStateException if {@code expression} is true - */ - public static void checkState(final boolean expression, String tag, String msg) { - if (!expression) { - warn(tag, "Illegal State", msg, new IllegalStateException(msg)); - } - } - - /** - * Throws or logs if an expression involving the state of the calling - * instance, but not involving any parameters to the calling method is not true. - * - * @param expression a boolean expression - * @throws IllegalStateException if {@code expression} is true - */ - public static void checkState(final boolean expression) { - checkState(expression, null, null); - } - - /** - * Throws or logs if the Feature is not enabled - * - * @param context an android context - * @param feature the required feature - * @param tag used to identify the source of a log message. It usually - * identifies the class or activity where the log call occurs - * @throws IllegalStateException if {@code feature} is not enabled - */ - public static void checkFeatureEnabled(Context context, Feature feature, String tag) { - checkState(feature.isEnabled(context), tag, feature.toString()); - } - - /** - * Throws a {@link RuntimeException} if {@link BuildConfig#ENG} is true, else log a warning. - * - * @param tag Used to identify the source of a log message. It usually - * identifies the class or activity where the log call occurs. - * @param msg The message you would like logged - * @param e The exception to throw - */ - public static void warn(String tag, String prefix, String msg, RuntimeException e) - throws RuntimeException{ - if (BuildConfig.ENG) { - throw e; - } else { - if (TextUtils.isEmpty(tag)) { - tag = TAG; - } - String logMessage; - if (TextUtils.isEmpty(msg)) { - logMessage = prefix; - } else if (TextUtils.isEmpty(prefix)) { - logMessage = msg; - } else { - logMessage = prefix + ": " + msg; - } - Log.w(tag, logMessage, e); - } - } - - private SoftPreconditions() { - } -} diff --git a/src/com/android/tv/util/SystemProperties.java b/src/com/android/tv/util/SystemProperties.java index 1dc70fd5..235161b6 100644 --- a/src/com/android/tv/util/SystemProperties.java +++ b/src/com/android/tv/util/SystemProperties.java @@ -58,13 +58,6 @@ public final class SystemProperties { public static final BooleanSystemProperty USE_TRACKER = new BooleanSystemProperty( "tv_use_tracker", true); - /** - * Selects using {@link com.android.tv.dvr.DvrDataManagerInMemoryImpl} - * instead of {@link com.android.tv.dvr.DvrDataManagerImpl} - */ - public static final BooleanSystemProperty USE_IN_MEMORY_DVR_DB = new BooleanSystemProperty( - "tv_use_in_memory_dvr_db", false); // STOPSHIP(DVR) - static { updateSystemProperties(); } diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java index 250ca430..b4149637 100644 --- a/src/com/android/tv/util/TvInputManagerHelper.java +++ b/src/com/android/tv/util/TvInputManagerHelper.java @@ -26,6 +26,7 @@ import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; +import com.android.tv.common.SoftPreconditions; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.parental.ParentalControlSettings; diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java index 44d601c5..a763fe58 100644 --- a/src/com/android/tv/util/Utils.java +++ b/src/com/android/tv/util/Utils.java @@ -24,10 +24,10 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.ColorStateList; +import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.Resources.Theme; import android.database.Cursor; -import android.media.tv.TvContentRating; import android.media.tv.TvContract; import android.media.tv.TvContract.Channels; import android.media.tv.TvInputInfo; @@ -42,18 +42,16 @@ import android.text.TextUtils; import android.text.format.DateUtils; import android.util.Log; import android.view.View; +import android.widget.Toast; -import com.android.tv.Features; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.data.StreamInfo; -import com.android.usbtuner.tvinput.UsbTunerTvInputService; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Date; @@ -336,6 +334,19 @@ public class Utils { return ""; } + public static String getAspectRatioString(float videoDisplayAspectRatio) { + if (videoDisplayAspectRatio <= 0) { + return ""; + } + + for (AspectRatio ratio : AspectRatio.values()) { + if (Math.abs((float) ratio.width / ratio.height - videoDisplayAspectRatio) < 0.05f) { + return ratio.toString(); + } + } + return ""; + } + public static int getVideoDefinitionLevelFromSize(int width, int height) { if (width >= VIDEO_ULTRA_HD_WIDTH && height >= VIDEO_ULTRA_HD_HEIGHT) { return StreamInfo.VIDEO_DEFINITION_LEVEL_ULTRA_HD; @@ -609,67 +620,46 @@ public class Utils { } /** - * Returns input ID of {@link UsbTunerTvInputService}. + * Returns a localized version of the text resource specified by resourceId. */ - @Nullable - public static String getUsbTunerInputId(Context context) { - if (!Features.USB_TUNER.isEnabled(context)) { - return null; + public static CharSequence getTextForLocale(Context context, Locale locale, int resourceId) { + if (locale.equals(context.getResources().getConfiguration().locale)) { + return context.getText(resourceId); } - return TvContract.buildInputId(new ComponentName(context.getPackageName(), - UsbTunerTvInputService.class.getName())); + Configuration config = new Configuration(context.getResources().getConfiguration()); + config.setLocale(locale); + return context.createConfigurationContext(config).getText(resourceId); } /** - * Returns {@link TvInputInfo} object of {@link UsbTunerTvInputService}. + * Returns the internal TV inputs. */ - @Nullable - public static TvInputInfo getUsbTunerInputInfo(Context context) { - if (!Features.USB_TUNER.isEnabled(context)) { - return null; + public static List<TvInputInfo> getInternalTvInputs(Context context, boolean tunerInputOnly) { + List<TvInputInfo> inputs = new ArrayList<>(); + String contextPackageName = context.getPackageName(); + for (TvInputInfo input : TvApplication.getSingletons(context).getTvInputManagerHelper() + .getTvInputInfos(true, tunerInputOnly)) { + if (contextPackageName.equals(ComponentName.unflattenFromString(input.getId()) + .getPackageName())) { + inputs.add(input); + } } - TvInputManagerHelper helper = TvApplication.getSingletons(context) - .getTvInputManagerHelper(); - return helper.getTvInputInfo(getUsbTunerInputId(context)); + return inputs; } - private static final class SyncRunnable implements Runnable { - private final Runnable mTarget; - private boolean mComplete; - - public SyncRunnable(Runnable target) { - mTarget = target; - } - - @Override - public void run() { - try { - mTarget.run(); - } finally { - synchronized (this) { - mComplete = true; - notifyAll(); - } - } - } + /** + * Checks whether the input is internal or not. + */ + public static boolean isInternalTvInput(Context context, String inputId) { + return context.getPackageName().equals(ComponentName.unflattenFromString(inputId) + .getPackageName()); + } - public void waitForComplete() { - boolean interrupted = false; - synchronized (this) { - try { - while (!mComplete) { - try { - wait(); - } catch (InterruptedException e) { - interrupted = true; - } - } - } finally { - if (interrupted) { - Thread.currentThread().interrupt(); - } - } - } - } + /** + * Shows a toast message to notice that the current feature is a developer feature. + */ + public static void showToastMessageForDeveloperFeature(Context context) { + Toast.makeText(context, "This feature is for developer preview.", Toast.LENGTH_SHORT) + .show(); } } diff --git a/src/com/android/usbtuner/UsbInputController.java b/src/com/android/usbtuner/UsbInputController.java index 6d6fccc2..f0982eb5 100644 --- a/src/com/android/usbtuner/UsbInputController.java +++ b/src/com/android/usbtuner/UsbInputController.java @@ -23,10 +23,14 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; +import android.media.tv.TvInputInfo; +import android.media.tv.TvInputManager; +import android.media.tv.TvInputService; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.support.v4.os.BuildCompat; import android.util.Log; import com.android.tv.Features; @@ -44,7 +48,7 @@ import java.util.Map; * to update the connection status of the supported USB TV tuners. */ public class UsbInputController extends BroadcastReceiver { - private static final boolean DEBUG = false; + private static final boolean DEBUG = true; private static final String TAG = "UsbInputController"; private static final TunerDevice[] TUNER_DEVICES = { @@ -58,7 +62,7 @@ public class UsbInputController extends BroadcastReceiver { private static final long DVB_DRIVER_CHECK_DELAY_MS = 300; private DvbDeviceAccessor mDvbDeviceAccessor; - private Handler mHandler = new Handler(Looper.getMainLooper()) { + private final Handler mHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { switch (msg.what) { @@ -155,6 +159,10 @@ public class UsbInputController extends BroadcastReceiver { PackageManager pm = context.getPackageManager(); ComponentName USBTUNER = new ComponentName(context, UsbTunerTvInputService.class); + // Don't kill app by enabling/disabling TvActivity. If LC is killed by enabling/disabling + // TvActivity, the following pm.setComponentEnabledSetting doesn't work. + ((TvApplication) context.getApplicationContext()).handleInputCountChanged( + true, enabled, true); // Since PackageManager.DONT_KILL_APP delays the operation by 10 seconds // (PackageManagerService.BROADCAST_DELAY), we'd better avoid using it. It is used only // when the LiveChannels app is active since we don't want to kill the running app. @@ -165,10 +173,17 @@ public class UsbInputController extends BroadcastReceiver { if (newState != pm.getComponentEnabledSetting(USBTUNER)) { // Send/cancel the USB tuner TV input setup recommendation card. TunerSetupActivity.onTvInputEnabled(context, enabled); - // Enable/disable the USB tuner TV input. pm.setComponentEnabledSetting(USBTUNER, newState, flags); if (DEBUG) Log.d(TAG, "Status updated:" + enabled); } + if (enabled && BuildCompat.isAtLeastN()) { + TvInputInfo info = mDvbDeviceAccessor.buildTvInputInfo(context); + if (info != null) { + Log.i(TAG, "TvInputInfo updated: " + info.toString()); + ((TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE)) + .updateTvInputInfo(info); + } + } } } |