diff options
Diffstat (limited to 'src/com')
225 files changed, 5202 insertions, 8347 deletions
diff --git a/src/com/android/tv/Features.java b/src/com/android/tv/Features.java index d8b7ae26..7e8e3689 100644 --- a/src/com/android/tv/Features.java +++ b/src/com/android/tv/Features.java @@ -27,18 +27,11 @@ import android.content.pm.PackageManager; import android.os.Build; import android.support.annotation.VisibleForTesting; import android.support.v4.os.BuildCompat; -import android.text.TextUtils; -import android.util.Log; import com.android.tv.common.feature.Feature; import com.android.tv.common.feature.GServiceFeature; import com.android.tv.common.feature.PropertyFeature; -import com.android.tv.config.RemoteConfig; -import com.android.tv.util.LocationUtils; import com.android.tv.util.PermissionUtils; -import com.android.tv.util.Utils; - -import java.util.Locale; /** * List of {@link Feature} for the Live TV App. @@ -46,9 +39,6 @@ import java.util.Locale; * <p>Remove the {@code Feature} once it is launched. */ public final class Features { - private static final String TAG = "Features"; - private static final boolean DEBUG = false; - /** * UI for opting in to analytics. * @@ -71,11 +61,6 @@ public final class Features { @Override public boolean isEnabled(Context context) { - if (Utils.isDeveloper()) { - // we enable tuner for developers to test tuner in any platform. - return true; - } - // 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()}. @@ -84,25 +69,6 @@ public final class Features { }; - /** - * Use network tuner if it is available and there is no other tuner types. - */ - public static final Feature NETWORK_TUNER = - new Feature() { - @Override - public boolean isEnabled(Context context) { - if (!TUNER.isEnabled(context)) { - return false; - } - if (Utils.isDeveloper()) { - // Network tuner will be enabled for developers. - return true; - } - return Locale.US.getCountry().equalsIgnoreCase( - LocationUtils.getCurrentCountry(context)); - } - }; - 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. @@ -130,36 +96,6 @@ public final class Features { }; /** - * Use AC3 software decode. - */ - public static final Feature AC3_SOFTWARE_DECODE = - new Feature() { - private final String[] SUPPORTED_COUNTRIES = { - }; - - private Boolean mEnabled; - - @Override - public boolean isEnabled(Context context) { - if (mEnabled == null) { - if (mEnabled == null) { - // We will not cache the result of fallback solution. - String country = LocationUtils.getCurrentCountry(context); - for (int i = 0; i < SUPPORTED_COUNTRIES.length; ++i) { - if (SUPPORTED_COUNTRIES[i].equalsIgnoreCase(country)) { - return true; - } - } - if (DEBUG) Log.d(TAG, "AC3 flag false after country check"); - return false; - } - } - if (DEBUG) Log.d(TAG, "AC3 flag " + mEnabled); - return mEnabled; - } - }; - - /** * Enable a conflict dialog between currently watched channel and upcoming recording. */ public static final Feature SHOW_UPCOMING_CONFLICT_DIALOG = OFF; diff --git a/src/com/android/tv/InputSessionManager.java b/src/com/android/tv/InputSessionManager.java index faf76555..e4b0f456 100644 --- a/src/com/android/tv/InputSessionManager.java +++ b/src/com/android/tv/InputSessionManager.java @@ -37,6 +37,7 @@ import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; +import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.ui.TunableTvView; import com.android.tv.ui.TunableTvView.OnTuneListener; @@ -72,8 +73,6 @@ public class InputSessionManager { Collections.synchronizedSet(new ArraySet<>()); private final Set<OnTvViewChannelChangeListener> mOnTvViewChannelChangeListeners = new ArraySet<>(); - private final Set<OnRecordingSessionChangeListener> mOnRecordingSessionChangeListeners = - new ArraySet<>(); public InputSessionManager(Context context) { mContext = context.getApplicationContext(); @@ -114,9 +113,6 @@ public class InputSessionManager { RecordingSession session = new RecordingSession(inputId, tag, callback, handler, endTimeMs); mRecordingSessions.add(session); if (DEBUG) Log.d(TAG, "Recording session created: " + session); - for (OnRecordingSessionChangeListener listener : mOnRecordingSessionChangeListeners) { - listener.onRecordingSessionChange(true, mRecordingSessions.size()); - } return session; } @@ -127,9 +123,6 @@ public class InputSessionManager { mRecordingSessions.remove(session); session.release(); if (DEBUG) Log.d(TAG, "Recording session released: " + session); - for (OnRecordingSessionChangeListener listener : mOnRecordingSessionChangeListeners) { - listener.onRecordingSessionChange(false, mRecordingSessions.size()); - } } /** @@ -155,17 +148,9 @@ public class InputSessionManager { } } - /** Adds the {@link OnRecordingSessionChangeListener}. */ - public void addOnRecordingSessionChangeListener(OnRecordingSessionChangeListener listener) { - mOnRecordingSessionChangeListeners.add(listener); - } - - /** Removes the {@link OnRecordingSessionChangeListener}. */ - public void removeRecordingSessionChangeListener(OnRecordingSessionChangeListener listener) { - mOnRecordingSessionChangeListeners.remove(listener); - } - - /** Returns the current {@link TvView} channel. */ + /** + * Returns the current {@link TvView} channel. + */ @MainThread public Uri getCurrentTvViewChannelUri() { for (TvViewSession session : mTvViewSessions) { @@ -561,9 +546,4 @@ public class InputSessionManager { public interface OnTvViewChannelChangeListener { void onTvViewChannelChange(@Nullable Uri channelUri); } - - /** Called when recording session is created or destroyed. */ - public interface OnRecordingSessionChangeListener { - void onRecordingSessionChange(boolean create, int count); - } } diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java index 94006b72..58850b5f 100644 --- a/src/com/android/tv/MainActivity.java +++ b/src/com/android/tv/MainActivity.java @@ -29,6 +29,7 @@ import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Point; import android.hardware.display.DisplayManager; import android.media.AudioManager; import android.media.MediaMetadata; @@ -70,6 +71,7 @@ import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; import android.widget.Toast; +import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.SendChannelStatusRunnable; import com.android.tv.analytics.SendConfigInfoRunnable; import com.android.tv.analytics.Tracker; @@ -89,14 +91,14 @@ import com.android.tv.data.ProgramDataManager; import com.android.tv.data.StreamInfo; import com.android.tv.data.WatchedHistoryManager; import com.android.tv.data.epg.EpgFetcher; -import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dialog.SafeDismissDialogFragment; +import com.android.tv.dvr.ConflictChecker; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.recorder.ConflictChecker; +import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.ui.DvrStopRecordingFragment; -import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.dvr.ui.HalfSizedDialogFragment; import com.android.tv.experiments.Experiments; import com.android.tv.menu.Menu; import com.android.tv.onboarding.OnboardingActivity; @@ -105,11 +107,9 @@ import com.android.tv.parental.ParentalControlSettings; import com.android.tv.receiver.AudioCapabilitiesReceiver; import com.android.tv.recommendation.NotificationService; import com.android.tv.search.ProgramGuideSearchFragment; -import com.android.tv.tuner.TunerInputController; import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.setup.TunerSetupActivity; import com.android.tv.tuner.tvinput.TunerTvInputService; -import com.android.tv.tuner.util.PostalCodeUtils; import com.android.tv.ui.AppLayerTvView; import com.android.tv.ui.ChannelBannerView; import com.android.tv.ui.InputBannerView; @@ -119,7 +119,6 @@ import com.android.tv.ui.SelectInputView.OnInputSelectedCallback; import com.android.tv.ui.TunableTvView; import com.android.tv.ui.TunableTvView.BlockScreenType; import com.android.tv.ui.TunableTvView.OnTuneListener; -import com.android.tv.ui.TuningBlockView; import com.android.tv.ui.TvOverlayManager; import com.android.tv.ui.TvViewUiManager; import com.android.tv.ui.sidepanel.ClosedCaptionFragment; @@ -131,20 +130,21 @@ import com.android.tv.ui.sidepanel.SettingsFragment; import com.android.tv.ui.sidepanel.SideFragment; import com.android.tv.util.AccountHelper; import com.android.tv.util.CaptionSettings; -import com.android.tv.util.Debug; -import com.android.tv.util.DurationTimer; import com.android.tv.util.ImageCache; import com.android.tv.util.ImageLoader; import com.android.tv.util.OnboardingUtils; import com.android.tv.util.PermissionUtils; +import com.android.tv.util.PipInputManager; +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.SystemProperties; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.TvSettings; +import com.android.tv.util.TvSettings.PipSound; import com.android.tv.util.TvTrackInfoUtils; import com.android.tv.util.Utils; -import com.android.tv.util.ViewCache; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -181,12 +181,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private static final float FRAME_RATE_FOR_FILM = 23.976f; private static final float FRAME_RATE_EPSILON = 0.1f; - private static PlaybackState MEDIA_SESSION_STATE_PLAYING = new PlaybackState.Builder() - .setState(PlaybackState.STATE_PLAYING, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 1.0f) - .build(); - private static PlaybackState MEDIA_SESSION_STATE_STOPPED = new PlaybackState.Builder() - .setState(PlaybackState.STATE_STOPPED, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 0.0f) - .build(); + private static final float MEDIA_SESSION_STOPPED_SPEED = 0.0f; + private static final float MEDIA_SESSION_PLAYING_SPEED = 1.0f; private static final int PERMISSIONS_REQUEST_READ_TV_LISTINGS = 1; @@ -231,12 +227,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private static final int MSG_CHANNEL_DOWN_PRESSED = 1000; private static final int MSG_CHANNEL_UP_PRESSED = 1001; + private static final int MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE = 1002; @Retention(RetentionPolicy.SOURCE) @IntDef({UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW, UPDATE_CHANNEL_BANNER_REASON_TUNE, UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST, UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO, - UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK, - UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO}) + UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK}) private @interface ChannelBannerUpdateReason {} /** * Updates channel banner because the channel banner is forced to show. @@ -258,10 +254,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC * Updates channel banner because the current watched channel is locked or unlocked. */ private static final int UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK = 5; - /** - * Updates channel banner because of stream info updating. - */ - private static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO = 6; private static final int TVVIEW_SET_MAIN_TIMEOUT_MS = 3000; @@ -269,13 +261,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC // Delay 1 second in order not to interrupt the first tune. private static final long LAZY_INITIALIZATION_DELAY = TimeUnit.SECONDS.toMillis(1); - private static final int UNDEFINED_TRACK_INDEX = -1; - private AccessibilityManager mAccessibilityManager; private ChannelDataManager mChannelDataManager; private ProgramDataManager mProgramDataManager; private TvInputManagerHelper mTvInputManagerHelper; private ChannelTuner mChannelTuner; + private PipInputManager mPipInputManager; private final TvOptionsManager mTvOptionsManager = new TvOptionsManager(this); private TvViewUiManager mTvViewUiManager; private TimeShiftManager mTimeShiftManager; @@ -287,6 +278,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private View mContentView; private TunableTvView mTvView; + private TunableTvView mPipView; private Bundle mTuneParams; private boolean mChannelBannerHiddenBySideFragment; // TODO: Move the scene views into TvTransitionManager or TvOverlayManager. @@ -311,6 +303,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private AudioManager mAudioManager; private int mAudioFocusStatus; private boolean mTunePending; + private boolean mPipEnabled; + private Channel mPipChannel; + private boolean mPipSwap; + @PipSound private int mPipSound = TvSettings.PIP_SOUND_MAIN; // Default private boolean mDebugNonFullSizeScreen; private boolean mActivityResumed; private boolean mActivityStarted; @@ -335,6 +331,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private boolean mIsCurrentChannelUnblockedByUser; private boolean mWasChannelUnblockedBeforeShrunkenByUser; private Channel mChannelBeforeShrunkenTvView; + private Channel mPipChannelBeforeShrunkenTvView; private boolean mIsCompletingShrunkenTvView; // TODO: Need to consider the case that TIS explicitly request PIN code while TV view is @@ -353,6 +350,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private RecurringRunner mSendConfigInfoRecurringRunner; private RecurringRunner mChannelStatusRecurringRunner; + // A caller which started this activity. (e.g. TvSearch) + private String mSource; + private final Handler mHandler = new MainActivityHandler(this); private final Set<OnActionClickListener> mOnActionClickListeners = new ArraySet<>(); @@ -372,13 +372,15 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (!mActivityResumed && mVisibleBehind) { // ACTION_SCREEN_ON is usually called after onResume. But, if media is played // under launcher with requestVisibleBehind(true), onResume will not be called. - // In this case, we need to resume TvView explicitly. + // In this case, we need to resume TvView and PipView explicitly. resumeTvIfNeeded(); + resumePipIfNeeded(); } } else if (intent.getAction().equals( TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED)) { if (DEBUG) Log.d(TAG, "Received parental control settings change"); - checkChannelLockNeeded(mTvView, null); + checkChannelLockNeeded(mTvView); + checkChannelLockNeeded(mPipView); applyParentalControlSettings(); } } @@ -405,11 +407,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC new ChannelTuner.Listener() { @Override public void onLoadFinished() { - Debug.getTimer(Debug.TAG_START_UP_TIMER).log( - "MainActivity.mChannelTunerListener.onLoadFinished"); SetupUtils.getInstance(MainActivity.this).markNewChannelsBrowsable(); if (mActivityResumed) { resumeTvIfNeeded(); + resumePipIfNeeded(); } mKeypadChannelSwitchView.setChannels(mChannelTuner.getBrowsableChannelList()); } @@ -429,12 +430,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } }; - private final Runnable mRestoreMainViewRunnable = new Runnable() { - @Override - public void run() { - restoreMainTvView(); - } - }; + private final Runnable mRestoreMainViewRunnable = + new Runnable() { + @Override + public void run() { + restoreMainTvView(); + } + }; private ProgramGuideSearchFragment mSearchFragment; private final TvInputCallback mTvInputCallback = new TvInputCallback() { @@ -454,14 +456,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC boolean parentalControlEnabled = mTvInputManagerHelper.getParentalControlSettings() .isParentalControlsEnabled(); mTvView.onParentalControlChanged(parentalControlEnabled); + mPipView.onParentalControlChanged(parentalControlEnabled); } @Override protected void onCreate(Bundle savedInstanceState) { - // Restarts the global duration timer to avoid the case that TvApplication starts much - // earlier than MainActivity. - Debug.getTimer(Debug.TAG_START_UP_TIMER).start(); - Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onCreate restarts timer"); if (DEBUG) Log.d(TAG,"onCreate()"); TvApplication.setCurrentRunningProcess(this, true); super.onCreate(savedInstanceState); @@ -479,14 +478,32 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return; } + // Check this permission for the EPG fetch. + // TODO: check {@link shouldShowRequestPermissionRationale}. + // While testing, no way to allow the permission when the dialog shows up. So we'd better + // not show the dialog. + if (!TvCommonUtils.isRunningInTest() && Utils.hasInternalTvInputs(this, true) + && checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION}, + PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION); + } + + DisplayManager displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE); + Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); + Point size = new Point(); + display.getSize(size); + int screenWidth = size.x; + int screenHeight = size.y; + mDefaultRefreshRate = display.getRefreshRate(); + setContentView(R.layout.activity_tv); - TvApplication tvApplication = (TvApplication) getApplication(); - mProgramDataManager = tvApplication.getProgramDataManager(); - mTvInputManagerHelper = tvApplication.getTvInputManagerHelper(); + mContentView = findViewById(android.R.id.content); mTvView = (TunableTvView) findViewById(R.id.main_tunable_tv_view); - mTvView.initialize((AppLayerTvView) findViewById(R.id.main_tv_view), - (TuningBlockView) findViewById(R.id.tuning_block), mProgramDataManager, - mTvInputManagerHelper); + int shrunkenTvViewHeight = getResources().getDimensionPixelSize( + R.dimen.shrunken_tvview_height); + mTvView.initialize((AppLayerTvView) findViewById(R.id.main_tv_view), false, screenHeight, + shrunkenTvViewHeight); mTvView.setOnUnhandledInputEventListener(new OnUnhandledInputEventListener() { @Override public boolean onUnhandledInputEvent(InputEvent event) { @@ -509,6 +526,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return false; } }); + long channelId = Utils.getLastWatchedChannelId(this); String inputId = Utils.getLastWatchedTunerInputId(this); if (!isPassthroughInput && inputId != null @@ -516,34 +534,27 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mTvView.warmUpInput(inputId, TvContract.buildChannelUri(channelId)); } - // Check this permission for the EPG fetch. - // TODO: check {@link shouldShowRequestPermissionRationale}. - // While testing, no way to allow the permission when the dialog shows up. So we'd better - // not show the dialog. - if (!TvCommonUtils.isRunningInTest() && Utils.hasInternalTvInputs(this, true) - && TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(this)) - && checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) - != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION}, - PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION); - } - + TvApplication tvApplication = (TvApplication) getApplication(); tvApplication.getMainActivityWrapper().onMainActivityCreated(this); if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) { Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show(); } mTracker = tvApplication.getTracker(); + mTvInputManagerHelper = tvApplication.getTvInputManagerHelper(); if (Features.TUNER.isEnabled(this)) { mTvInputManagerHelper.addCallback(mTvInputCallback); } mTunerInputId = TunerTvInputService.getInputId(this); mChannelDataManager = tvApplication.getChannelDataManager(); + mProgramDataManager = tvApplication.getProgramDataManager(); mProgramDataManager.addOnCurrentProgramUpdatedListener(Channel.INVALID_ID, mOnCurrentProgramUpdatedListener); mProgramDataManager.setPrefetchEnabled(true); mChannelTuner = new ChannelTuner(mChannelDataManager, mTvInputManagerHelper); mChannelTuner.addListener(mChannelTunerListener); mChannelTuner.start(); + mPipInputManager = new PipInputManager(this, mTvInputManagerHelper, mChannelTuner); + mPipInputManager.start(); mMemoryManageables.add(mProgramDataManager); mMemoryManageables.add(ImageCache.getInstance()); mMemoryManageables.add(TvContentRatingCache.getInstance()); @@ -573,9 +584,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } }); - DisplayManager displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE); - Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); - mDefaultRefreshRate = display.getRefreshRate(); + mPipView = (TunableTvView) findViewById(R.id.pip_tunable_tv_view); + mPipView.initialize((AppLayerTvView) findViewById(R.id.pip_tv_view), true, screenHeight, + shrunkenTvViewHeight); if (!PermissionUtils.hasAccessWatchedHistory(this)) { WatchedHistoryManager watchedHistoryManager = new WatchedHistoryManager( @@ -583,10 +594,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC watchedHistoryManager.start(); mTvView.setWatchedHistoryManager(watchedHistoryManager); } - mTvViewUiManager = new TvViewUiManager(this, mTvView, + mTvViewUiManager = new TvViewUiManager(this, mTvView, mPipView, (FrameLayout) findViewById(android.R.id.content), mTvOptionsManager); - mContentView = findViewById(android.R.id.content); + mPipView.setFixedSurfaceSize(screenWidth / 2, screenHeight / 2); + mPipView.setBlockScreenType(TunableTvView.BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW); + ViewGroup sceneContainer = (ViewGroup) findViewById(R.id.scene_container); mChannelBannerView = (ChannelBannerView) getLayoutInflater().inflate( R.layout.channel_banner, sceneContainer, false); @@ -628,7 +641,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } }); mSearchFragment = new ProgramGuideSearchFragment(); - mOverlayManager = new TvOverlayManager(this, mChannelTuner, mTvView, mTvOptionsManager, + mOverlayManager = new TvOverlayManager(this, mChannelTuner, mTvView, mKeypadChannelSwitchView, mChannelBannerView, inputBannerView, selectInputView, sceneContainer, mSearchFragment); @@ -679,7 +692,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mDvrConflictChecker = new ConflictChecker(this); } initForTest(); - Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onCreate end"); } @Override @@ -714,7 +726,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC case PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED && Experiments.CLOUD_EPG.get()) { - EpgFetcher.getInstance(this).startImmediately(false); + EpgFetcher.getInstance(this).startImmediately(); + } else { + EpgFetcher.getInstance(this).stop(); } break; } @@ -784,14 +798,14 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC Intent notificationIntent = new Intent(this, NotificationService.class); notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION); startService(notificationIntent); - TunerInputController.executeNetworkTunerDiscoveryAsyncTask(this); } @Override protected void onResume() { - Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume start"); if (DEBUG) Log.d(TAG, "onResume()"); super.onResume(); + // Refresh the remote config, it is throttled automatically. + TvApplication.getSingletons(this).getRemoteConfig().fetch(null); if (!PermissionUtils.hasAccessAllEpg(this) && checkSelfPermission(PERMISSION_READ_TV_LISTINGS) != PackageManager.PERMISSION_GRANTED) { @@ -816,16 +830,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC // visible behind. requestVisibleBehind(true); } - Set<String> failedScheduledRecordingInfoSet = - Utils.getFailedScheduledRecordingInfoSet(getApplicationContext()); - if (Utils.hasRecordingFailedReason( - getApplicationContext(), TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE) - && !failedScheduledRecordingInfoSet.isEmpty()) { + if (Utils.hasRecordingFailedReason(getApplicationContext(), + TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE)) { runAfterAttachedToWindow(new Runnable() { @Override public void run() { - DvrUiHelper.showDvrInsufficientSpaceErrorDialog(MainActivity.this, - failedScheduledRecordingInfoSet); + DvrUiHelper.showDvrInsufficientSpaceErrorDialog(MainActivity.this); } }); } @@ -833,6 +843,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (mChannelTuner.areAllChannelsLoaded()) { SetupUtils.getInstance(this).markNewChannelsBrowsable(); resumeTvIfNeeded(); + resumePipIfNeeded(); } mOverlayManager.showMenuWithTimeShiftPauseIfNeeded(); @@ -870,7 +881,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (mDvrConflictChecker != null) { mDvrConflictChecker.start(); } - Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume end"); } @Override @@ -883,6 +893,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mActivityResumed = false; mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_DEFAULT); mTvView.setBlockScreenType(TunableTvView.BLOCK_SCREEN_TYPE_NO_UI); + if (mPipEnabled) { + mTvViewUiManager.hidePipForPause(); + } mBackKeyPressed = false; mShowLockedChannelsTemporarily = false; mShouldTuneToTunerChannel = false; @@ -890,7 +903,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; mAudioManager.abandonAudioFocus(this); if (mMediaSession.isActive()) { - setMediaSessionActiveAndPlaybackState(false); + mMediaSession.setActive(false); } mTracker.sendScreenView(""); } else { @@ -949,6 +962,21 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mTvView.setBlockScreenType(getDesiredBlockScreenType()); } + private void resumePipIfNeeded() { + if (mPipEnabled && !(mPipView.isPlaying() && mPipView.isShown())) { + if (mPipInputManager.areInSamePipInput( + mChannelTuner.getCurrentChannel(), mPipChannel)) { + enablePipView(false, false); + } else { + if (!mPipView.isPlaying()) { + startPip(false); + } else { + mTvViewUiManager.showPipForResume(); + } + } + } + } + private void startTv(Uri channelUri) { if (DEBUG) Log.d(TAG, "startTv Uri=" + channelUri); if ((channelUri == null || !TvContract.isChannelUriForPassthroughInput(channelUri)) @@ -999,9 +1027,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } - mTvView.start(); + mTvView.start(mTvInputManagerHelper); setVolumeByAudioFocusStatus(); - tune(true); + tune(); } @Override @@ -1044,6 +1072,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private void stopAll(boolean keepVisibleBehind) { mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION); stopTv("stopAll()", keepVisibleBehind); + stopPip(); } public TvInputManagerHelper getTvInputManagerHelper() { @@ -1116,6 +1145,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return mProgramDataManager; } + public PipInputManager getPipInputManager() { + return mPipInputManager; + } + public TvOptionsManager getTvOptionsManager() { return mTvOptionsManager; } @@ -1239,11 +1272,19 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mChannelBeforeShrunkenTvView = mTvView.getCurrentChannel(); mWasChannelUnblockedBeforeShrunkenByUser = mIsCurrentChannelUnblockedByUser; mAllowedRatingBeforeShrunken = mLastAllowedRatingForCurrentChannel; + + if (willMainViewBeTunerInput && mChannelTuner.isCurrentChannelPassthrough() + && mPipEnabled) { + mPipChannelBeforeShrunkenTvView = mPipChannel; + enablePipView(false, false); + } else { + mPipChannelBeforeShrunkenTvView = null; + } mTvViewUiManager.startShrunkenTvView(); if (showLockedChannelsTemporarily) { mShowLockedChannelsTemporarily = true; - checkChannelLockNeeded(mTvView, null); + checkChannelLockNeeded(mTvView); } mTvView.setBlockScreenType(getDesiredBlockScreenType()); @@ -1279,15 +1320,23 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mIsCompletingShrunkenTvView = false; mIsCurrentChannelUnblockedByUser = mWasChannelUnblockedBeforeShrunkenByUser; mTvView.setBlockScreenType(getDesiredBlockScreenType()); + if (mPipChannelBeforeShrunkenTvView != null) { + enablePipView(true, false); + mPipChannelBeforeShrunkenTvView = null; + } } }; mTvViewUiManager.fadeOutTvView(tuneAction); // Will automatically fade-in when video becomes available. } else { - checkChannelLockNeeded(mTvView, null); + checkChannelLockNeeded(mTvView); mIsCompletingShrunkenTvView = false; mIsCurrentChannelUnblockedByUser = mWasChannelUnblockedBeforeShrunkenByUser; mTvView.setBlockScreenType(getDesiredBlockScreenType()); + if (mPipChannelBeforeShrunkenTvView != null) { + enablePipView(true, false); + mPipChannelBeforeShrunkenTvView = null; + } } } @@ -1314,7 +1363,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mChannelTuner.moveToAdjacentBrowsableChannel(true); } if (mTunePending) { - tune(true); + tune(); } } else { mInputIdUnderSetup = null; @@ -1439,6 +1488,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC }); } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { Uri uri = intent.getData(); + try { + mSource = uri.getQueryParameter(Utils.PARAM_SOURCE); + } catch (UnsupportedOperationException e) { + // ignore this exception. + } // When the URI points to the programs (directory, not an individual item), go to the // program guide. The intention here is to respond to // "content://android.media.tv/program", not "content://android.media.tv/program/XXX". @@ -1514,20 +1568,38 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } private void setVolumeByAudioFocusStatus() { - if (mTvView.isPlaying()) { + if (mPipSound == TvSettings.PIP_SOUND_MAIN) { + setVolumeByAudioFocusStatus(mTvView); + } else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW + setVolumeByAudioFocusStatus(mPipView); + } + } + + private void setVolumeByAudioFocusStatus(TunableTvView tvView) { + SoftPreconditions.checkState(tvView == mTvView || tvView == mPipView); + if (tvView.isPlaying()) { switch (mAudioFocusStatus) { case AudioManager.AUDIOFOCUS_GAIN: - mTvView.setStreamVolume(AUDIO_MAX_VOLUME); + tvView.setStreamVolume(AUDIO_MAX_VOLUME); break; case AudioManager.AUDIOFOCUS_LOSS: case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - mTvView.setStreamVolume(AUDIO_MIN_VOLUME); + tvView.setStreamVolume(AUDIO_MIN_VOLUME); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: - mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME); + tvView.setStreamVolume(AUDIO_DUCKING_VOLUME); 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() { @@ -1547,7 +1619,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } mAudioManager.abandonAudioFocus(this); if (mMediaSession.isActive()) { - setMediaSessionActiveAndPlaybackState(false); + mMediaSession.setActive(false); } } TvApplication.getSingletons(this).getMainActivityWrapper() @@ -1560,11 +1632,95 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return mTvView.isPlaying() && mTvView.getCurrentChannel() != null; } + private void startPip(final boolean fromUserInteraction) { + if (mPipChannel == null) { + Log.w(TAG, "PIP channel id is an invalid id."); + return; + } + if (DEBUG) Log.d(TAG, "startPip() " + mPipChannel); + mPipView.start(mTvInputManagerHelper); + boolean success = mPipView.tuneTo(mPipChannel, null, new OnTuneListener() { + @Override + public void onUnexpectedStop(Channel channel) { + Log.w(TAG, "The PIP is Unexpectedly stopped"); + enablePipView(false, false); + } + + @Override + public void onTuneFailed(Channel channel) { + Log.w(TAG, "Fail to start the PIP during channel tuning"); + if (fromUserInteraction) { + Toast.makeText(MainActivity.this, R.string.msg_no_pip_support, + Toast.LENGTH_SHORT).show(); + enablePipView(false, false); + } + } + + @Override + public void onStreamInfoChanged(StreamInfo info) { + mTvViewUiManager.updatePipView(); + 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 from PIP input but can't find a channel" + + " with the URI " + channel); + return; + } + if (isChannelChangeKeyDownReceived()) { + // Ignore this message if the user is changing the channel. + return; + } + mPipChannel = currentChannel; + mPipView.setCurrentChannel(mPipChannel); + } + + @Override + public void onContentBlocked() { + updateMediaSession(); + } + + @Override + public void onContentAllowed() { + updateMediaSession(); + } + }); + if (!success) { + Log.w(TAG, "Fail to start the PIP"); + return; + } + if (fromUserInteraction) { + checkChannelLockNeeded(mPipView); + } + // Explicitly make the PIP view main to make the selected input an HDMI-CEC active source. + mPipView.setMain(); + scheduleRestoreMainTvView(); + mTvViewUiManager.onPipStart(); + setVolumeByAudioFocusStatus(); + } + private void scheduleRestoreMainTvView() { mHandler.removeCallbacks(mRestoreMainViewRunnable); mHandler.postDelayed(mRestoreMainViewRunnable, TVVIEW_SET_MAIN_TIMEOUT_MS); } + private void stopPip() { + if (DEBUG) Log.d(TAG, "stopPip"); + if (mPipView.isPlaying()) { + mPipView.stop(); + mPipSwap = false; + mTvViewUiManager.onPipStop(); + } + } + /** * Says {@code text} when accessibility is turned on. */ @@ -1579,7 +1735,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } - private void tune(boolean updateChannelBanner) { + private void tune() { if (DEBUG) Log.d(TAG, "tune()"); mTuneDurationTimer.start(); @@ -1669,6 +1825,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (!isUnderShrunkenTvView()) { mLastAllowedRatingForCurrentChannel = null; } + mHandler.removeMessages(MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE); // For every tune, we need to inform the tuned channel or input to a user, // if Talkback is turned on. sendAccessibilityText(!mChannelTuner.isCurrentChannelPassthrough() ? @@ -1695,13 +1852,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC TvApplication.getSingletons(this).getMainActivityWrapper() .notifyCurrentChannelChange(this, channel); } - // We have to provide channel here instead of using TvView's channel, because TvView's - // channel might be null when there's tuner conflict. In that case, TvView will resets - // its current channel onConnectionFailed(). - checkChannelLockNeeded(mTvView, channel); - if (updateChannelBanner) { - updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE); - } + checkChannelLockNeeded(mTvView); + updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE); if (mActivityResumed) { // requestVisibleBehind should be called after onResume() is called. But, when // launcher is over the TV app and the screen is turned off and on, tune() can @@ -1745,18 +1897,19 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private void updateMediaSession() { if (getCurrentChannel() == null) { - setMediaSessionActiveAndPlaybackState(false); + mMediaSession.setActive(false); return; } // If the channel is blocked, display a lock and a short text on the Now Playing Card if (mTvView.isScreenBlocked() || mTvView.getBlockedContentRating() != null) { + setMediaSessionPlaybackState(false); Bitmap art = BitmapFactory.decodeResource( getResources(), R.drawable.ic_message_lock_preview); updateMediaMetadata( getResources().getString(R.string.channel_banner_locked_channel_title), art); - setMediaSessionActiveAndPlaybackState(true); + mMediaSession.setActive(true); return; } @@ -1771,11 +1924,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC cardTitleText = getCurrentChannelName(); } updateMediaMetadata(cardTitleText, null); + setMediaSessionPlaybackState(true); + if (posterArtUri == null) { posterArtUri = TvContract.buildChannelLogoUri(getCurrentChannelId()).toString(); } updatePosterArt(getCurrentChannel(), currentProgram, cardTitleText, null, posterArtUri); - setMediaSessionActiveAndPlaybackState(true); + mMediaSession.setActive(true); } private void updatePosterArt(Channel currentChannel, Program currentProgram, @@ -1862,17 +2017,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } - private void setMediaSessionActiveAndPlaybackState(boolean isPlaying) { - if (isPlaying) { - mMediaSession.setActive(true); - // setMediaSessionPlaybackState has to be called after calling mMediaSession.setActive - // b/31933276 - mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_PLAYING); - } else { - mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_STOPPED); - mMediaSession.setActive(false); - } - + private void setMediaSessionPlaybackState(boolean isPlaying) { + PlaybackState.Builder builder = new PlaybackState.Builder(); + builder.setState(isPlaying ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_STOPPED, + PlaybackState.PLAYBACK_POSITION_UNKNOWN, + isPlaying ? MEDIA_SESSION_PLAYING_SPEED : MEDIA_SESSION_STOPPED_SPEED); + mMediaSession.setPlaybackState(builder.build()); } private void addToRecentChannels(long channelId) { @@ -1892,27 +2042,33 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return mRecentChannels; } - private void checkChannelLockNeeded(TunableTvView tvView, Channel currentChannel) { - if (currentChannel == null) { - currentChannel = tvView.getCurrentChannel(); - } - if (tvView.isPlaying() && currentChannel != null) { + private void checkChannelLockNeeded(TunableTvView tvView) { + Channel channel = tvView.getCurrentChannel(); + if (tvView.isPlaying() && channel != null) { if (getParentalControlSettings().isParentalControlsEnabled() - && currentChannel.isLocked() + && channel.isLocked() && !mShowLockedChannelsTemporarily && !(isUnderShrunkenTvView() - && currentChannel.equals(mChannelBeforeShrunkenTvView) + && channel.equals(mChannelBeforeShrunkenTvView) && mWasChannelUnblockedBeforeShrunkenByUser)) { - if (DEBUG) Log.d(TAG, "Channel " + currentChannel.getId() + " is locked"); - blockOrUnblockScreen(tvView, true); + if (DEBUG) Log.d(TAG, "Channel " + channel.getId() + " is locked"); + blockScreen(tvView); } else { - blockOrUnblockScreen(tvView, false); + unblockScreen(tvView); } } } - private void blockOrUnblockScreen(TunableTvView tvView, boolean blockOrUnblock) { - tvView.blockOrUnblockScreen(blockOrUnblock); + private void blockScreen(TunableTvView tvView) { + tvView.blockScreen(); + if (tvView == mTvView) { + updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); + updateMediaSession(); + } + } + + private void unblockScreen(TunableTvView tvView) { + tvView.unblockScreen(); if (tvView == mTvView) { updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); updateMediaSession(); @@ -1933,59 +2089,36 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private void updateChannelBannerAndShowIfNeeded(@ChannelBannerUpdateReason int reason) { if(DEBUG) Log.d(TAG, "updateChannelBannerAndShowIfNeeded(reason=" + reason + ")"); - if (isChannelChangeKeyDownReceived() && reason != UPDATE_CHANNEL_BANNER_REASON_TUNE - && reason != UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST) { - // Tuning is still ongoing, no need to update banner for other reasons - return; - } if (!mChannelTuner.isCurrentChannelPassthrough()) { int lockType = ChannelBannerView.LOCK_NONE; - if (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST) { - if (getParentalControlSettings().isParentalControlsEnabled() - && getCurrentChannel().isLocked()) { - lockType = ChannelBannerView.LOCK_CHANNEL_INFO; - } else { - // Do not show detailed program information while fast-tuning. - lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL; - } - } else if (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE) { - if (getParentalControlSettings().isParentalControlsEnabled()) { - if (getCurrentChannel().isLocked()) { - lockType = ChannelBannerView.LOCK_CHANNEL_INFO; - } else { - // If parental control is turned on, - // assumes that program is locked by default and waits for onContentAllowed. - lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL; - } - } - } else if (mTvView.isScreenBlocked()) { + if (mTvView.isScreenBlocked()) { lockType = ChannelBannerView.LOCK_CHANNEL_INFO; } else if (mTvView.getBlockedContentRating() != null || (getParentalControlSettings().isParentalControlsEnabled() - && !mTvView.isVideoOrAudioAvailable())) { + && !mTvView.isVideoAvailable())) { // If the parental control is enabled, do not show the program detail until the // video becomes available. lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL; } + if (lockType == ChannelBannerView.LOCK_NONE) { + if (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST) { + // Do not show detailed program information while fast-tuning. + lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL; + } else if (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE + && getParentalControlSettings().isParentalControlsEnabled()) { + // If parental control is turned on, + // assumes that program is locked by default and waits for onContentAllowed. + lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL; + } + } // If lock type is not changed, we don't need to update channel banner by parental // control. - int previousLockType = mChannelBannerView.setLockType(lockType); - if (previousLockType == lockType + if (!mChannelBannerView.setLockType(lockType) && reason == UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK) { return; - } else if (reason == UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO) { - mChannelBannerView.updateStreamInfo(mTvView); - // If parental control is enabled, we shows program description when the video is - // available, instead of tuning. Therefore we need to check it here if the program - // description is previously hidden by parental control. - if (previousLockType == ChannelBannerView.LOCK_PROGRAM_DETAIL && - lockType != ChannelBannerView.LOCK_PROGRAM_DETAIL) { - mChannelBannerView.updateViews(false); - } - } else { - mChannelBannerView.updateViews(reason == UPDATE_CHANNEL_BANNER_REASON_TUNE - || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST); } + + mChannelBannerView.updateViews(mTvView); } boolean needToShowBanner = (reason == UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE @@ -2064,7 +2197,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (bestTrack != null) { String selectedTrack = getSelectedTrack(TvTrackInfo.TYPE_AUDIO); if (!bestTrack.getId().equals(selectedTrack)) { - selectTrack(TvTrackInfo.TYPE_AUDIO, bestTrack, UNDEFINED_TRACK_INDEX); + selectTrack(TvTrackInfo.TYPE_AUDIO, bestTrack); } else { mTvOptionsManager.onMultiAudioChanged( Utils.getMultiAudioString(this, bestTrack, false)); @@ -2077,7 +2210,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private void applyClosedCaption() { List<TvTrackInfo> tracks = getTracks(TvTrackInfo.TYPE_SUBTITLE); if (tracks == null) { - mTvOptionsManager.onClosedCaptionsChanged(null, UNDEFINED_TRACK_INDEX); + mTvOptionsManager.onClosedCaptionsChanged(null); return; } @@ -2086,19 +2219,17 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC String selectedTrackId = getSelectedTrack(TvTrackInfo.TYPE_SUBTITLE); TvTrackInfo alternativeTrack = null; - int alternativeTrackIndex = UNDEFINED_TRACK_INDEX; if (enabled) { String language = mCaptionSettings.getLanguage(); String trackId = mCaptionSettings.getTrackId(); - for (int i = 0; i < tracks.size(); i++) { - TvTrackInfo track = tracks.get(i); + for (TvTrackInfo track : tracks) { if (Utils.isEqualLanguage(track.getLanguage(), language)) { if (track.getId().equals(trackId)) { if (!track.getId().equals(selectedTrackId)) { - selectTrack(TvTrackInfo.TYPE_SUBTITLE, track, i); + selectTrack(TvTrackInfo.TYPE_SUBTITLE, track); } else { // Already selected. Update the option string only. - mTvOptionsManager.onClosedCaptionsChanged(track, i); + mTvOptionsManager.onClosedCaptionsChanged(track); } if (DEBUG) { Log.d(TAG, "Subtitle Track Selected {id=" + track.getId() @@ -2107,16 +2238,14 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return; } else if (alternativeTrack == null) { alternativeTrack = track; - alternativeTrackIndex = i; } } } if (alternativeTrack != null) { if (!alternativeTrack.getId().equals(selectedTrackId)) { - selectTrack(TvTrackInfo.TYPE_SUBTITLE, alternativeTrack, alternativeTrackIndex); + selectTrack(TvTrackInfo.TYPE_SUBTITLE, alternativeTrack); } else { - mTvOptionsManager - .onClosedCaptionsChanged(alternativeTrack, alternativeTrackIndex); + mTvOptionsManager.onClosedCaptionsChanged(alternativeTrack); } if (DEBUG) { Log.d(TAG, "Subtitle Track Selected {id=" + alternativeTrack.getId() @@ -2126,11 +2255,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } if (selectedTrackId != null) { - selectTrack(TvTrackInfo.TYPE_SUBTITLE, null, UNDEFINED_TRACK_INDEX); + selectTrack(TvTrackInfo.TYPE_SUBTITLE, null); if (DEBUG) Log.d(TAG, "Subtitle Track Unselected"); return; } - mTvOptionsManager.onClosedCaptionsChanged(null, UNDEFINED_TRACK_INDEX); + mTvOptionsManager.onClosedCaptionsChanged(null); } /** @@ -2145,6 +2274,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } + public void showSearchActivity() { + // HACK: Once we moved the window layer to TYPE_APPLICATION_SUB_PANEL, + // the voice button doesn't work. So we directly call the voice action. + SearchManagerHelper.getInstance(this).launchAssistAction(); + } + public void showProgramGuideSearchFragment() { getFragmentManager().beginTransaction().replace(R.id.fragment_container, mSearchFragment) .addToBackStack(null).commit(); @@ -2160,11 +2295,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC @Override protected void onDestroy() { if (DEBUG) Log.d(TAG, "onDestroy()"); - SideFragment.releaseRecycledViewPool(); - ViewCache.getInstance().clear(); + SideFragment.releasePreloadedRecycledViews(); if (mTvView != null) { mTvView.release(); } + if (mPipView != null) { + mPipView.release(); + } if (mChannelTuner != null) { mChannelTuner.removeListener(mChannelTunerListener); mChannelTuner.stop(); @@ -2177,6 +2314,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mProgramDataManager.setPrefetchEnabled(false); } } + if (mPipInputManager != null) { + mPipInputManager.stop(); + } if (mOverlayManager != null) { mOverlayManager.release(); } @@ -2200,11 +2340,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mChannelStatusRecurringRunner.stop(); mChannelStatusRecurringRunner = null; } - if (mTvInputManagerHelper != null) { - mTvInputManagerHelper.clearTvInputLabels(); - if (Features.TUNER.isEnabled(this)) { - mTvInputManagerHelper.removeCallback(mTvInputCallback); - } + if (mTvInputManagerHelper != null && Features.TUNER.isEnabled(this)) { + mTvInputManagerHelper.removeCallback(mTvInputCallback); } super.onDestroy(); } @@ -2273,6 +2410,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC * G debug: refresh cloud epg * I KEYCODE_TV_INPUT * O debug: show display mode option + * P debug: togglePipView * S KEYCODE_CAPTIONS: select subtitle * W debug: toggle screen size * V KEYCODE_MEDIA_RECORD debug: record the current channel for 30 sec @@ -2284,9 +2422,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC finishChannelChangeIfNeeded(); if (event.getKeyCode() == KeyEvent.KEYCODE_SEARCH) { - // Prevent MainActivity from being closed by onVisibleBehindCanceled() - mOtherActivityLaunched = true; - return false; + showSearchActivity(); + return true; } switch (mOverlayManager.onKeyUp(keyCode, event)) { case KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY: @@ -2335,7 +2472,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } switch (keyCode) { case KeyEvent.KEYCODE_DPAD_RIGHT: - if (!mTvView.isVideoOrAudioAvailable() + if (!mTvView.isVideoAvailable() && mTvView.getVideoUnavailableReason() == TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE) { DvrUiHelper.startSchedulesActivityForTuneConflict(this, @@ -2354,7 +2491,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC @Override public void done(boolean success) { if (success) { - blockOrUnblockScreen(mTvView, false); + unblockScreen(mTvView); mIsCurrentChannelUnblockedByUser = true; } } @@ -2441,17 +2578,22 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC Toast.makeText(this, R.string.dvr_msg_cannot_record_program, Toast.LENGTH_SHORT).show(); } else { + if (!DvrUiHelper.checkStorageStatusAndShowErrorMessage(this, + currentChannel.getInputId())) { + return true; + } Program program = mProgramDataManager .getCurrentProgram(currentChannel.getId()); - DvrUiHelper.checkStorageStatusAndShowErrorMessage(this, - currentChannel.getInputId(), new Runnable() { - @Override - public void run() { - DvrUiHelper.requestRecordingCurrentProgram( - MainActivity.this, - currentChannel, program, false); - } - }); + if (program == null) { + DvrUiHelper + .showChannelRecordDurationOptions(this, currentChannel); + } else if (DvrUiHelper.handleCreateSchedule(this, program)) { + String msg = getString( + R.string.dvr_msg_current_program_scheduled, + program.getTitle(), Utils.toTimeString( + program.getEndTimeUtcMillis(), false)); + Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); + } } } else { DvrUiHelper.showStopRecordingDialog(this, currentChannel.getId(), @@ -2501,6 +2643,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } return true; } + case KeyEvent.KEYCODE_P: { + togglePipView(); + return true; + } case KeyEvent.KEYCODE_CTRL_LEFT: case KeyEvent.KEYCODE_CTRL_RIGHT: { mUseKeycodeBlacklist = !mUseKeycodeBlacklist; @@ -2535,6 +2681,22 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } @Override + public void onBackPressed() { + // The activity should be returned to the caller of this activity + // when the mSource is not null. + if (!mOverlayManager.getSideFragmentManager().isActive() && isPlaying() + && mSource == null) { + // If back key would exit TV app, + // show McLauncher instead so we can get benefit of McLauncher's shyMode. + Intent startMain = new Intent(Intent.ACTION_MAIN); + startMain.addCategory(Intent.CATEGORY_HOME); + startActivity(startMain); + } else { + super.onBackPressed(); + } + } + + @Override public void onUserInteraction() { super.onUserInteraction(); if (mOverlayManager != null) { @@ -2563,6 +2725,65 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } + public void togglePipView() { + enablePipView(!mPipEnabled, true); + mOverlayManager.getMenu().update(); + } + + public boolean isPipEnabled() { + return mPipEnabled; + } + + public void tuneToChannelForPip(Channel channel) { + if (!mPipEnabled) { + throw new IllegalStateException("tuneToChannelForPip is called when PIP is off"); + } + if (mPipChannel.equals(channel)) { + return; + } + mPipChannel = channel; + startPip(true); + } + + private void enablePipView(boolean enable, boolean fromUserInteraction) { + if (enable == mPipEnabled) { + return; + } + if (enable) { + List<PipInput> pipAvailableInputs = mPipInputManager.getPipInputList(true); + if (pipAvailableInputs.isEmpty()) { + Toast.makeText(this, R.string.msg_no_available_input_by_pip, Toast.LENGTH_SHORT) + .show(); + return; + } + // TODO: choose the last pip input. + Channel pipChannel = pipAvailableInputs.get(0).getChannel(); + if (pipChannel != null) { + mPipEnabled = true; + mPipChannel = pipChannel; + startPip(fromUserInteraction); + mTvViewUiManager.restorePipSize(); + mTvViewUiManager.restorePipLayout(); + mTvOptionsManager.onPipChanged(mPipEnabled); + } else { + Toast.makeText(this, R.string.msg_no_available_input_by_pip, Toast.LENGTH_SHORT) + .show(); + } + } else { + mPipEnabled = false; + mPipChannel = null; + // Recover the stream volume of the main TV view, if needed. + if (mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW) { + setVolumeByAudioFocusStatus(mTvView); + mPipSound = TvSettings.PIP_SOUND_MAIN; + mTvOptionsManager.onPipSoundChanged(mPipSound); + } + stopPip(); + mTvViewUiManager.restoreDisplayMode(false); + mTvOptionsManager.onPipChanged(mPipEnabled); + } + } + private boolean isChannelChangeKeyDownReceived() { return mHandler.hasMessages(MSG_CHANNEL_UP_PRESSED) || mHandler.hasMessages(MSG_CHANNEL_DOWN_PRESSED); @@ -2590,6 +2811,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (SystemProperties.LOG_KEYEVENT.getValue()) { Log.d(TAG, "dispatchKeyEventToSession(" + event + ")"); } + if (mPipEnabled && mChannelTuner.isCurrentChannelPassthrough()) { + // If PIP is enabled, key events will be used by UI. + return false; + } boolean handled = false; if (mTvView != null) { handled = mTvView.dispatchKeyEvent(event); @@ -2607,15 +2832,21 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } private boolean isKeyEventBlocked() { - // If the current channel is a passthrough channel, we don't handle the key events in TV - // activity. Instead, the key event will be handled by the passthrough TV input. - return mChannelTuner.isCurrentChannelPassthrough(); + // If the current channel is passthrough channel without a PIP view, + // we always don't handle the key events in TV activity. Instead, the key event will + // be handled by the passthrough TV input. + return mChannelTuner.isCurrentChannelPassthrough() && !mPipEnabled; } private void tuneToLastWatchedChannelForTunerInput() { if (!mChannelTuner.isCurrentChannelPassthrough()) { return; } + if (mPipEnabled) { + if (!mPipChannel.isPassthrough()) { + enablePipView(false, true); + } + } stopTv(); startTv(null); } @@ -2626,16 +2857,16 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mTvView.reset(); } } else { + if (mPipEnabled && mPipInputManager.areInSamePipInput(channel, mPipChannel)) { + enablePipView(false, true); + } if (!mTvView.isPlaying()) { startTv(channel.getUri()); } else if (channel.equals(mTvView.getCurrentChannel())) { updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE); - } else if (channel == mChannelTuner.getCurrentChannel()) { - // Channel banner is already updated in moveToAdjacentChannel - tune(false); } else if (mChannelTuner.moveToChannel(channel)) { // Channel banner would be updated inside of tune. - tune(true); + tune(); } else { showSettingsFragment(); } @@ -2657,11 +2888,90 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } + public Channel getPipChannel() { + return mPipChannel; + } + + /** + * Swap the main and the sub screens while in the PIP mode. + */ + public void swapPip() { + if (!mPipEnabled || mTvView == null || mPipView == null) { + Log.e(TAG, "swapPip() - not in PIP"); + mPipSwap = false; + return; + } + + Channel channel = mTvView.getCurrentChannel(); + boolean tvViewBlocked = mTvView.isScreenBlocked(); + boolean pipViewBlocked = mPipView.isScreenBlocked(); + if (channel == null || !mTvView.isPlaying()) { + // If the TV view is not currently playing or its current channel is null, swapping here + // basically means disabling the PIP mode and getting back to the full screen since + // there's no point of keeping a blank PIP screen at the bottom which is not tune-able. + enablePipView(false, true); + mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_DEFAULT); + mPipSwap = false; + return; + } + + // Reset the TV view and tune the PIP view to the previous channel of the TV view. + mTvView.reset(); + mPipView.reset(); + Channel oldPipChannel = mPipChannel; + tuneToChannelForPip(channel); + if (tvViewBlocked) { + mPipView.blockScreen(); + } else { + mPipView.unblockScreen(); + } + + if (oldPipChannel != null) { + // Tune the TV view to the previous PIP channel. + tuneToChannel(oldPipChannel); + } + if (pipViewBlocked) { + mTvView.blockScreen(); + } else { + mTvView.unblockScreen(); + } + if (mPipSound == TvSettings.PIP_SOUND_MAIN) { + setVolumeByAudioFocusStatus(mTvView); + } else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW + setVolumeByAudioFocusStatus(mPipView); + } + mPipSwap = !mPipSwap; + mTvOptionsManager.onPipSwapChanged(mPipSwap); + } + + /** + * Toggle where the sound is coming from when the user is watching the PIP. + */ + public void togglePipSoundMode() { + if (!mPipEnabled || mTvView == null || mPipView == null) { + Log.e(TAG, "togglePipSoundMode() - not in PIP"); + return; + } + if (mPipSound == TvSettings.PIP_SOUND_MAIN) { + setVolumeByAudioFocusStatus(mPipView); + mPipSound = TvSettings.PIP_SOUND_PIP_WINDOW; + } else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW + setVolumeByAudioFocusStatus(mTvView); + mPipSound = TvSettings.PIP_SOUND_MAIN; + } + restoreMainTvView(); + mTvOptionsManager.onPipSoundChanged(mPipSound); + } + /** * Set the main TV view which holds HDMI-CEC active source based on the sound mode */ private void restoreMainTvView() { - mTvView.setMain(); + if (mPipSound == TvSettings.PIP_SOUND_MAIN) { + mTvView.setMain(); + } else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW + mPipView.setMain(); + } } @Override @@ -2671,8 +2981,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; mAudioManager.abandonAudioFocus(this); if (mMediaSession.isActive()) { - setMediaSessionActiveAndPlaybackState(false); + mMediaSession.setActive(false); } + 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 @@ -2701,13 +3012,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return mTvView.getSelectedTrack(type); } - private void selectTrack(int type, TvTrackInfo track, int trackIndex) { + private void selectTrack(int type, TvTrackInfo track) { mTvView.selectTrack(type, track == null ? null : track.getId()); if (type == TvTrackInfo.TYPE_AUDIO) { mTvOptionsManager.onMultiAudioChanged(track == null ? null : Utils.getMultiAudioString(this, track, false)); } else if (type == TvTrackInfo.TYPE_SUBTITLE) { - mTvOptionsManager.onClosedCaptionsChanged(track, trackIndex); + mTvOptionsManager.onClosedCaptionsChanged(track); } } @@ -2852,7 +3163,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (mActivityStarted) { initAnimations(); initSideFragments(); - initMenuItemViews(); } } }, LAZY_INITIALIZATION_DELAY); @@ -2864,11 +3174,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } private void initSideFragments() { - SideFragment.preloadItemViews(this); - } - - private void initMenuItemViews() { - mOverlayManager.getMenu().preloadItemViews(); + SideFragment.preloadRecycledViews(this); } @Override @@ -2901,6 +3207,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC sendMessageDelayed(Message.obtain(msg), getDelay(startTime)); mainActivity.moveToAdjacentChannel(true, true); break; + case MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE: + mainActivity.updateChannelBannerAndShowIfNeeded( + UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO); + break; } } @@ -2915,12 +3225,14 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private class MyOnTuneListener implements OnTuneListener { boolean mUnlockAllowedRatingBeforeShrunken = true; boolean mWasUnderShrunkenTvView; + long mStreamInfoUpdateTimeThresholdMs; Channel mChannel; public MyOnTuneListener() { } private void onTune(Channel channel, boolean wasUnderShrukenTvView) { - Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.MyOnTuneListener.onTune"); + mStreamInfoUpdateTimeThresholdMs = + System.currentTimeMillis() + FIRST_STREAM_INFO_UPDATE_DELAY_MILLIS; mChannel = channel; mWasUnderShrunkenTvView = wasUnderShrukenTvView; } @@ -2946,9 +3258,20 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mTracker.sendChannelTuneTime(info.getCurrentChannel(), mTuneDurationTimer.reset()); } - if (info.isVideoOrAudioAvailable() && mChannel == getCurrentChannel()) { - updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO); + // 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(); @@ -2985,9 +3308,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC @Override public void onContentBlocked() { - Debug.getTimer(Debug.TAG_START_UP_TIMER).log( - "MainActivity.MyOnTuneListener.onContentBlocked removes timer"); - Debug.removeTimer(Debug.TAG_START_UP_TIMER); mTuneDurationTimer.reset(); TvContentRating rating = mTvView.getBlockedContentRating(); // When tuneTo was called while TV view was shrunken, if the channel id is the same @@ -2999,10 +3319,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView(); mTvView.unblockContent(rating); } - if (!isChannelChangeKeyDownReceived()) { - mChannelBannerView.setBlockingContentRating(rating); - updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); - } + mChannelBannerView.setBlockingContentRating(rating); + updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); mTvViewUiManager.fadeInTvView(); } @@ -3011,10 +3329,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (!isUnderShrunkenTvView()) { mUnlockAllowedRatingBeforeShrunken = false; } - if (!isChannelChangeKeyDownReceived()) { - mChannelBannerView.setBlockingContentRating(null); - updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); - } + mChannelBannerView.setBlockingContentRating(null); + updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java index 6459693b..8a263a26 100644 --- a/src/com/android/tv/SetupPassthroughActivity.java +++ b/src/com/android/tv/SetupPassthroughActivity.java @@ -44,72 +44,64 @@ public class SetupPassthroughActivity extends Activity { private TvInputInfo mTvInputInfo; private Intent mActivityAfterCompletion; - private boolean mEpgFetcherDuringScan; @Override public void onCreate(Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); + Intent intent = getIntent(); + SoftPreconditions.checkState( + intent.getAction().equals(TvCommonConstants.INTENT_ACTION_INPUT_SETUP)); ApplicationSingletons appSingletons = TvApplication.getSingletons(this); TvInputManagerHelper inputManager = appSingletons.getTvInputManagerHelper(); - Intent intent = getIntent(); String inputId = intent.getStringExtra(TvCommonConstants.EXTRA_INPUT_ID); mTvInputInfo = inputManager.getTvInputInfo(inputId); + if (DEBUG) Log.d(TAG, "TvInputId " + inputId + " / TvInputInfo " + mTvInputInfo); + if (mTvInputInfo == null) { + Log.w(TAG, "There is no input with the ID " + inputId + "."); + finish(); + return; + } + Intent setupIntent = intent.getExtras().getParcelable(TvCommonConstants.EXTRA_SETUP_INTENT); + if (DEBUG) Log.d(TAG, "Setup activity launch intent: " + setupIntent); + if (setupIntent == null) { + Log.w(TAG, "The input (" + mTvInputInfo.getId() + ") doesn't have setup."); + finish(); + return; + } + SetupUtils.grantEpgPermission(this, mTvInputInfo.getServiceInfo().packageName); mActivityAfterCompletion = intent.getParcelableExtra( TvCommonConstants.EXTRA_ACTIVITY_AFTER_COMPLETION); - boolean needToFetchEpg = Utils.isInternalTvInput(this, mTvInputInfo.getId()) - && Experiments.CLOUD_EPG.get(); - if (needToFetchEpg) { - // In case when the activity is restored, this flag should be restored as well. - mEpgFetcherDuringScan = true; + if (DEBUG) Log.d(TAG, "Activity after completion " + mActivityAfterCompletion); + // If EXTRA_SETUP_INTENT is not removed, an infinite recursion happens during + // setupIntent.putExtras(intent.getExtras()). + Bundle extras = intent.getExtras(); + extras.remove(TvCommonConstants.EXTRA_SETUP_INTENT); + setupIntent.putExtras(extras); + try { + startActivityForResult(setupIntent, REQUEST_START_SETUP_ACTIVITY); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Can't find activity: " + setupIntent.getComponent()); + finish(); + return; } - if (savedInstanceState == null) { - SoftPreconditions.checkState( - intent.getAction().equals(TvCommonConstants.INTENT_ACTION_INPUT_SETUP)); - if (DEBUG) Log.d(TAG, "TvInputId " + inputId + " / TvInputInfo " + mTvInputInfo); - if (mTvInputInfo == null) { - Log.w(TAG, "There is no input with the ID " + inputId + "."); - finish(); - return; - } - Intent setupIntent = - intent.getExtras().getParcelable(TvCommonConstants.EXTRA_SETUP_INTENT); - if (DEBUG) Log.d(TAG, "Setup activity launch intent: " + setupIntent); - if (setupIntent == null) { - Log.w(TAG, "The input (" + mTvInputInfo.getId() + ") doesn't have setup."); - finish(); - return; - } - SetupUtils.grantEpgPermission(this, mTvInputInfo.getServiceInfo().packageName); - if (DEBUG) Log.d(TAG, "Activity after completion " + mActivityAfterCompletion); - // If EXTRA_SETUP_INTENT is not removed, an infinite recursion happens during - // setupIntent.putExtras(intent.getExtras()). - Bundle extras = intent.getExtras(); - extras.remove(TvCommonConstants.EXTRA_SETUP_INTENT); - setupIntent.putExtras(extras); - try { - startActivityForResult(setupIntent, REQUEST_START_SETUP_ACTIVITY); - } catch (ActivityNotFoundException e) { - Log.e(TAG, "Can't find activity: " + setupIntent.getComponent()); - finish(); - return; - } - if (needToFetchEpg) { - EpgFetcher.getInstance(this).onChannelScanStarted(); - } + if (Utils.isInternalTvInput(this, mTvInputInfo.getId()) && Experiments.CLOUD_EPG.get()) { + EpgFetcher.getInstance(this).stop(); } } @Override + protected void onDestroy() { + if (mTvInputInfo != null && Utils.isInternalTvInput(this, mTvInputInfo.getId()) + && Experiments.CLOUD_EPG.get()) { + EpgFetcher.getInstance(this).start(); + } + super.onDestroy(); + } + + @Override public void onActivityResult(int requestCode, final int resultCode, final Intent data) { - if (DEBUG) Log.d(TAG, "onActivityResult"); boolean setupComplete = requestCode == REQUEST_START_SETUP_ACTIVITY && resultCode == Activity.RESULT_OK; - // Tells EpgFetcher that channel source setup is finished. - if (mEpgFetcherDuringScan) { - EpgFetcher.getInstance(this).onChannelScanFinished(); - mEpgFetcherDuringScan = false; - } if (!setupComplete) { setResult(resultCode, data); finish(); @@ -130,4 +122,4 @@ public class SetupPassthroughActivity extends Activity { } }); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java index e1024705..2d6d45c4 100644 --- a/src/com/android/tv/TimeShiftManager.java +++ b/src/com/android/tv/TimeShiftManager.java @@ -887,11 +887,10 @@ public class TimeShiftManager { } long fetchStartTimeMs = Utils.floorTime(startTimeMs, MAX_DUMMY_PROGRAM_DURATION); - long fetchEndTimeMs = Utils.ceilTime(endTimeMs + PREFETCH_DURATION_FOR_NEXT, - MAX_DUMMY_PROGRAM_DURATION); - boolean needToLoad = addDummyPrograms(fetchStartTimeMs, fetchEndTimeMs); + boolean needToLoad = addDummyPrograms(fetchStartTimeMs, + endTimeMs + PREFETCH_DURATION_FOR_NEXT); if (needToLoad) { - Range<Long> period = Range.create(fetchStartTimeMs, fetchEndTimeMs); + Range<Long> period = Range.create(fetchStartTimeMs, endTimeMs); mProgramLoadQueue.add(period); startTaskIfNeeded(); } @@ -1013,7 +1012,7 @@ public class TimeShiftManager { for (int i = 0, j = 0; i < mPrograms.size() && j < loadedPrograms.size(); ++j) { Program loadedProgram = loadedPrograms.get(j); // Skip previous programs. - while (program.getEndTimeUtcMillis() <= loadedProgram.getStartTimeUtcMillis()) { + while (program.getEndTimeUtcMillis() < loadedProgram.getStartTimeUtcMillis()) { // Reached end of mPrograms. if (++i == mPrograms.size()) { return; diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java index 159df7b6..0e18a259 100644 --- a/src/com/android/tv/TvApplication.java +++ b/src/com/android/tv/TvApplication.java @@ -37,7 +37,6 @@ import android.util.Log; import android.view.KeyEvent; import com.android.tv.analytics.Analytics; -import com.android.tv.util.Debug; import com.android.tv.analytics.StubAnalytics; import com.android.tv.analytics.StubAnalytics; import com.android.tv.analytics.Tracker; @@ -54,10 +53,10 @@ import com.android.tv.data.ProgramDataManager; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManagerImpl; import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrRecordingService; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.DvrStorageStatusManager; import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.recorder.DvrRecordingService; import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.tvinput.TunerTvInputService; import com.android.tv.tuner.util.TunerInputInfoUtils; @@ -107,8 +106,6 @@ public class TvApplication extends Application implements ApplicationSingletons @Override public void onCreate() { - Debug.getTimer(Debug.TAG_START_UP_TIMER).start(); - Debug.getTimer(Debug.TAG_START_UP_TIMER).log("TvApplication.onCreate start"); super.onCreate(); SharedPreferencesUtils.initialize(this, new Runnable() { @Override @@ -152,13 +149,13 @@ public class TvApplication extends Application implements ApplicationSingletons mAnalytics = StubAnalytics.getInstance(this); } mTracker = mAnalytics.getDefaultTracker(); - getTvInputManagerHelper(); + mTvInputManagerHelper = new TvInputManagerHelper(this); + mTvInputManagerHelper.start(); // In SetupFragment, transitions are set in the constructor. Because the fragment can be // created in Activity.onCreate() by the framework, SetupAnimationHelper should be // initialized here before Activity.onCreate() is called. SetupAnimationHelper.initialize(this); Log.i(TAG, "Started Live TV " + mVersionName); - Debug.getTimer(Debug.TAG_START_UP_TIMER).log("TvApplication.onCreate end"); } private void setCurrentRunningProcess(boolean isMainProcess) { @@ -170,10 +167,8 @@ public class TvApplication extends Application implements ApplicationSingletons if (CommonFeatures.DVR.isEnabled(this)) { mDvrStorageStatusManager = new DvrStorageStatusManager(this, mRunningInMainProcess); } - // Fetch remote config - getSingletons(this).getRemoteConfig().fetch(null); if (mRunningInMainProcess) { - getTvInputManagerHelper().addCallback(new TvInputCallback() { + mTvInputManagerHelper.addCallback(new TvInputCallback() { @Override public void onInputAdded(String inputId) { if (Features.TUNER.isEnabled(TvApplication.this) && TextUtils.equals(inputId, @@ -273,7 +268,7 @@ public class TvApplication extends Application implements ApplicationSingletons @Override public ChannelDataManager getChannelDataManager() { if (mChannelDataManager == null) { - mChannelDataManager = new ChannelDataManager(this, getTvInputManagerHelper()); + mChannelDataManager = new ChannelDataManager(this, mTvInputManagerHelper); mChannelDataManager.start(); } return mChannelDataManager; @@ -319,10 +314,6 @@ public class TvApplication extends Application implements ApplicationSingletons */ @Override public TvInputManagerHelper getTvInputManagerHelper() { - if (mTvInputManagerHelper == null) { - mTvInputManagerHelper = new TvInputManagerHelper(this); - mTvInputManagerHelper.start(); - } return mTvInputManagerHelper; } diff --git a/src/com/android/tv/TvOptionsManager.java b/src/com/android/tv/TvOptionsManager.java index 493e039c..7871cbe7 100644 --- a/src/com/android/tv/TvOptionsManager.java +++ b/src/com/android/tv/TvOptionsManager.java @@ -18,13 +18,14 @@ package com.android.tv; import android.content.Context; import android.media.tv.TvTrackInfo; -import android.support.annotation.IntDef; import android.util.SparseArray; import com.android.tv.data.DisplayMode; +import com.android.tv.util.TvSettings; +import com.android.tv.util.TvSettings.PipLayout; +import com.android.tv.util.TvSettings.PipSize; +import com.android.tv.util.TvSettings.PipSound; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.Locale; /** @@ -32,34 +33,39 @@ import java.util.Locale; * captions and display mode. Can be also used to create MenuAction items to control such options. */ public class TvOptionsManager { - @Retention(RetentionPolicy.SOURCE) - @IntDef({OPTION_CLOSED_CAPTIONS, OPTION_DISPLAY_MODE, OPTION_SYSTEMWIDE_PIP, OPTION_MULTI_AUDIO, - OPTION_MORE_CHANNELS, OPTION_DEVELOPER, OPTION_SETTINGS}) - public @interface OptionType {} public static final int OPTION_CLOSED_CAPTIONS = 0; public static final int OPTION_DISPLAY_MODE = 1; - public static final int OPTION_SYSTEMWIDE_PIP = 2; - public static final int OPTION_MULTI_AUDIO = 3; - public static final int OPTION_MORE_CHANNELS = 4; - public static final int OPTION_DEVELOPER = 5; - public static final int OPTION_SETTINGS = 6; + 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_DEVELOPER = 6; + public static final int OPTION_SETTINGS = 7; + + public static final int OPTION_PIP_INPUT = 100; + public static final int OPTION_PIP_SWAP = 101; + public static final int OPTION_PIP_SOUND = 102; + public static final int OPTION_PIP_LAYOUT = 103 ; + public static final int OPTION_PIP_SIZE = 104; private final Context mContext; private final SparseArray<OptionChangedListener> mOptionChangedListeners = new SparseArray<>(); private String mClosedCaptionsLanguage; private int mDisplayMode; + private boolean mPip; private String mMultiAudio; + private String mPipInput; + private boolean mPipSwap; + @PipSound private int mPipSound; + @PipLayout private int mPipLayout; + @PipSize private int mPipSize; public TvOptionsManager(Context context) { mContext = context; } - /** - * Returns a suitable displayed string for the given option type under current settings. - * @param option the type of option, should be one of {@link OptionType}. - */ - public String getOptionString(@OptionType int option) { + public String getOptionString(int option) { switch (option) { case OPTION_CLOSED_CAPTIONS: if (mClosedCaptionsLanguage == null) { @@ -71,48 +77,101 @@ public class TvOptionsManager { .isDisplayModeAvailable(mDisplayMode) ? DisplayMode.getLabel(mDisplayMode, mContext) : DisplayMode.getLabel(DisplayMode.MODE_NORMAL, mContext); + 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: return mMultiAudio; + case OPTION_PIP_INPUT: + return mPipInput; + case OPTION_PIP_SWAP: + return mContext.getString(mPipSwap ? R.string.pip_options_item_swap_on + : R.string.pip_options_item_swap_off); + case OPTION_PIP_SOUND: + if (mPipSound == TvSettings.PIP_SOUND_MAIN) { + return mContext.getString(R.string.pip_options_item_sound_main); + } else if (mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW) { + return mContext.getString(R.string.pip_options_item_sound_pip_window); + } + break; + case OPTION_PIP_LAYOUT: + if (mPipLayout == TvSettings.PIP_LAYOUT_BOTTOM_RIGHT) { + return mContext.getString(R.string.pip_options_item_layout_bottom_right); + } else if (mPipLayout == TvSettings.PIP_LAYOUT_TOP_RIGHT) { + return mContext.getString(R.string.pip_options_item_layout_top_right); + } else if (mPipLayout == TvSettings.PIP_LAYOUT_TOP_LEFT) { + return mContext.getString(R.string.pip_options_item_layout_top_left); + } else if (mPipLayout == TvSettings.PIP_LAYOUT_BOTTOM_LEFT) { + return mContext.getString(R.string.pip_options_item_layout_bottom_left); + } else if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { + return mContext.getString(R.string.pip_options_item_layout_side_by_side); + } + break; + case OPTION_PIP_SIZE: + if (mPipSize == TvSettings.PIP_SIZE_BIG) { + return mContext.getString(R.string.pip_options_item_size_big); + } else if (mPipSize == TvSettings.PIP_SIZE_SMALL) { + return mContext.getString(R.string.pip_options_item_size_small); + } + break; } return ""; } - /** - * Handles changing selection of closed caption. - */ - public void onClosedCaptionsChanged(TvTrackInfo track, int trackIndex) { - mClosedCaptionsLanguage = (track == null) ? - null : (track.getLanguage() != null) ? track.getLanguage() - : mContext.getString(R.string.closed_caption_unknown_language, trackIndex + 1); + public void onClosedCaptionsChanged(TvTrackInfo track) { + mClosedCaptionsLanguage = (track == null) ? null + : (track.getLanguage() != null) ? track.getLanguage() + : mContext.getString(R.string.default_language); notifyOptionChanged(OPTION_CLOSED_CAPTIONS); } - /** - * Handles changing selection of display mode. - */ public void onDisplayModeChanged(int displayMode) { mDisplayMode = displayMode; notifyOptionChanged(OPTION_DISPLAY_MODE); } - /** - * Handles changing selection of multi-audio. - */ + public void onPipChanged(boolean pip) { + mPip = pip; + notifyOptionChanged(OPTION_IN_APP_PIP); + } + public void onMultiAudioChanged(String multiAudio) { mMultiAudio = multiAudio; notifyOptionChanged(OPTION_MULTI_AUDIO); } - private void notifyOptionChanged(@OptionType int option) { + public void onPipInputChanged(String pipInput) { + mPipInput = pipInput; + notifyOptionChanged(OPTION_PIP_INPUT); + } + + public void onPipSwapChanged(boolean pipSwap) { + mPipSwap = pipSwap; + notifyOptionChanged(OPTION_PIP_SWAP); + } + + public void onPipSoundChanged(@PipSound int pipSound) { + mPipSound = pipSound; + notifyOptionChanged(OPTION_PIP_SOUND); + } + + public void onPipLayoutChanged(@PipLayout int pipLayout) { + mPipLayout = pipLayout; + notifyOptionChanged(OPTION_PIP_LAYOUT); + } + + public void onPipSizeChanged(@PipSize int pipSize) { + mPipSize = pipSize; + notifyOptionChanged(OPTION_PIP_SIZE); + } + + private void notifyOptionChanged(int option) { OptionChangedListener listener = mOptionChangedListeners.get(option); if (listener != null) { - listener.onOptionChanged(option, getOptionString(option)); + listener.onOptionChanged(getOptionString(option)); } } - /** - * Sets listeners to changes of the given option type. - */ public void setOptionChangedListener(int option, OptionChangedListener listener) { mOptionChangedListeners.put(option, listener); } @@ -121,6 +180,6 @@ public class TvOptionsManager { * An interface used to monitor option changes. */ public interface OptionChangedListener { - void onOptionChanged(@OptionType int optionType, String newString); + void onOptionChanged(String newOption); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/util/DurationTimer.java b/src/com/android/tv/analytics/DurationTimer.java index b6221496..ad2d91f8 100644 --- a/src/com/android/tv/util/DurationTimer.java +++ b/src/com/android/tv/analytics/DurationTimer.java @@ -14,30 +14,17 @@ * limitations under the License. */ -package com.android.tv.util; +package com.android.tv.analytics; import android.os.SystemClock; -import android.util.Log; - -import com.android.tv.common.BuildConfig; /** * Times a duration. */ public final class DurationTimer { - private static final String TAG = "DurationTimer"; public static final long TIME_NOT_SET = -1; private long startTimeMs = TIME_NOT_SET; - private String mTag = TAG; - private boolean mLogEngOnly; - - public DurationTimer() { } - - public DurationTimer(String tag, boolean logEngOnly) { - mTag = tag; - mLogEngOnly = logEngOnly; - } /** * Returns true if the timer is running. @@ -72,13 +59,4 @@ public final class DurationTimer { startTimeMs = TIME_NOT_SET; return duration; } - - /** - * Adds information and duration time to the log. - */ - public void log(String message) { - if (isRunning() && (!mLogEngOnly || BuildConfig.ENG)) { - Log.i(mTag, message + " : " + getDuration() + "ms"); - } - } } diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java index 4da56311..30f84236 100644 --- a/src/com/android/tv/data/Channel.java +++ b/src/com/android/tv/data/Channel.java @@ -52,16 +52,6 @@ public final class Channel { public static final int LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART = 3; /** - * Compares the channel numbers of channels which belong to the same input. - */ - public static final Comparator<Channel> CHANNEL_NUMBER_COMPARATOR = new Comparator<Channel>() { - @Override - public int compare(Channel lhs, Channel rhs) { - return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber()); - } - }; - - /** * When a TIS doesn't provide any information about app link, and it doesn't have a leanback * launch intent, there will be no app link card for the TIS. */ @@ -97,15 +87,9 @@ public final class Channel { TvContract.Channels.COLUMN_APP_LINK_ICON_URI, TvContract.Channels.COLUMN_APP_LINK_POSTER_ART_URI, TvContract.Channels.COLUMN_APP_LINK_INTENT_URI, - TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, // Only used in bundled input }; /** - * Channel number delimiter between major and minor parts. - */ - public static final char CHANNEL_NUMBER_DELIMITER = '-'; - - /** * Creates {@code Channel} object from cursor. * * <p>The query that created the cursor MUST use {@link #PROJECTION} @@ -119,7 +103,7 @@ public final class Channel { channel.mPackageName = Utils.intern(cursor.getString(index++)); channel.mInputId = Utils.intern(cursor.getString(index++)); channel.mType = Utils.intern(cursor.getString(index++)); - channel.mDisplayNumber = normalizeDisplayNumber(cursor.getString(index++)); + channel.mDisplayNumber = cursor.getString(index++); channel.mDisplayName = cursor.getString(index++); channel.mDescription = cursor.getString(index++); channel.mVideoFormat = Utils.intern(cursor.getString(index++)); @@ -130,29 +114,17 @@ public final class Channel { channel.mAppLinkIconUri = cursor.getString(index++); channel.mAppLinkPosterArtUri = cursor.getString(index++); channel.mAppLinkIntentUri = cursor.getString(index++); - if (Utils.isBundledInput(channel.mInputId)) { - channel.mRecordingProhibited = cursor.getInt(index++) != 0; - } return channel; } /** - * Replaces the channel number separator with dash('-'). + * Creates a {@link Channel} object from the DVR database. */ - public static String normalizeDisplayNumber(String string) { - if (!TextUtils.isEmpty(string)) { - int length = string.length(); - for (int i = 0; i < length; i++) { - char c = string.charAt(i); - if (c == '.' || Character.isWhitespace(c) - || Character.getType(c) == Character.DASH_PUNCTUATION) { - StringBuilder sb = new StringBuilder(string); - sb.setCharAt(i, CHANNEL_NUMBER_DELIMITER); - return sb.toString(); - } - } - } - return string; + public static Channel fromDvrCursor(Cursor c) { + Channel channel = new Channel(); + int index = -1; + channel.mDvrId = c.getLong(++index); + return channel; } /** ID of this channel. Matches to BaseColumns._ID. */ @@ -175,10 +147,8 @@ public final class Channel { private String mAppLinkIntentUri; private Intent mAppLinkIntent; private int mAppLinkType; - private String mLogoUri; - private boolean mRecordingProhibited; - private boolean mChannelLogoExist; + private long mDvrId; private Channel() { // Do nothing. @@ -260,14 +230,10 @@ public final class Channel { } /** - * Returns channel logo uri which is got from cloud, it's used only for ChannelLogoFetcher. + * Returns an ID in DVR database. */ - public String getLogoUri() { - return mLogoUri; - } - - public boolean isRecordingProhibited() { - return mRecordingProhibited; + public long getDvrId() { + return mDvrId; } /** @@ -313,13 +279,6 @@ public final class Channel { } /** - * Sets channel logo uri which is got from cloud. - */ - public void setLogoUri(String logoUri) { - mLogoUri = logoUri; - } - - /** * Check whether {@code other} has same read-only channel info as this. But, it cannot check two * channels have same logos. It also excludes browsable and locked, because two fields are * changed by TV app. @@ -339,8 +298,7 @@ public final class Channel { && mAppLinkColor == other.mAppLinkColor && Objects.equals(mAppLinkIconUri, other.mAppLinkIconUri) && Objects.equals(mAppLinkPosterArtUri, other.mAppLinkPosterArtUri) - && Objects.equals(mAppLinkIntentUri, other.mAppLinkIntentUri) - && Objects.equals(mRecordingProhibited, other.mRecordingProhibited); + && Objects.equals(mAppLinkIntentUri, other.mAppLinkIntentUri); } @Override @@ -357,8 +315,7 @@ public final class Channel { + ", isPassthrough=" + mIsPassthrough + ", browsable=" + mBrowsable + ", locked=" + mLocked - + ", appLinkText=" + mAppLinkText - + ", recordingProhibited=" + mRecordingProhibited + "}"; + + ", appLinkText=" + mAppLinkText + "}"; } void copyFrom(Channel other) { @@ -383,8 +340,6 @@ public final class Channel { mAppLinkIntentUri = other.mAppLinkIntentUri; mAppLinkIntent = other.mAppLinkIntent; mAppLinkType = other.mAppLinkType; - mRecordingProhibited = other.mRecordingProhibited; - mChannelLogoExist = other.mChannelLogoExist; } /** @@ -434,6 +389,8 @@ public final class Channel { mChannel.mDisplayName = "name"; mChannel.mDescription = "description"; mChannel.mBrowsable = true; + mChannel.mLocked = false; + mChannel.mIsPassthrough = false; } public Builder(Channel other) { @@ -465,7 +422,7 @@ public final class Channel { @VisibleForTesting public Builder setDisplayNumber(String displayNumber) { - mChannel.mDisplayNumber = normalizeDisplayNumber(displayNumber); + mChannel.mDisplayNumber = displayNumber; return this; } @@ -528,11 +485,6 @@ public final class Channel { return this; } - public Builder setRecordingProhibited(boolean recordingProhibited) { - mChannel.mRecordingProhibited = recordingProhibited; - return this; - } - public Channel build() { Channel channel = new Channel(); channel.copyFrom(mChannel); @@ -572,21 +524,6 @@ public final class Channel { } /** - * Sets if the channel logo exists. This method should be only called from - * {@link ChannelDataManager}. - */ - void setChannelLogoExist(boolean exist) { - mChannelLogoExist = exist; - } - - /** - * Returns if channel logo exists. - */ - public boolean channelLogoExists() { - return mChannelLogoExist; - } - - /** * Returns the type of app link for this channel. * It returns {@link #APP_LINK_TYPE_CHANNEL} if the channel has a non null app link text and * a valid app link intent, it returns {@link #APP_LINK_TYPE_APP} if the input service which @@ -718,4 +655,4 @@ public final class Channel { return label; } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java index eb3871fc..6f9ea6d7 100644 --- a/src/com/android/tv/data/ChannelDataManager.java +++ b/src/com/android/tv/data/ChannelDataManager.java @@ -21,14 +21,10 @@ import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; -import android.content.res.AssetFileDescriptor; import android.database.ContentObserver; -import android.database.sqlite.SQLiteException; import android.media.tv.TvContract; import android.media.tv.TvContract.Channels; import android.media.tv.TvInputManager.TvInputCallback; -import android.net.Uri; -import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -47,8 +43,6 @@ import com.android.tv.util.PermissionUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; -import java.io.FileNotFoundException; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -198,6 +192,7 @@ public class ChannelDataManager { mStarted = false; mDbLoadFinished = false; + ChannelLogoFetcher.stopFetchingChannelLogos(); mInputManager.removeCallback(mTvInputCallback); mContentResolver.unregisterContentObserver(mChannelObserver); mHandler.removeCallbacksAndMessages(null); @@ -595,36 +590,6 @@ public class ChannelDataManager { } } - private class checkChannelLogoExistTask extends AsyncTask<Void, Void, Boolean> { - private final Channel mChannel; - - public checkChannelLogoExistTask(Channel channel) { - mChannel = channel; - } - - @Override - protected Boolean doInBackground(Void... params) { - boolean result = false; - try { - AssetFileDescriptor f = mContext.getContentResolver().openAssetFileDescriptor( - TvContract.buildChannelLogoUri(mChannel.getId()), "r"); - result = true; - f.close(); - } catch (SQLiteException | IOException | NullPointerException e) { - // File not found or asset file not found. - } - return result; - } - - @Override - protected void onPostExecute(Boolean result) { - ChannelWrapper wrapper = mChannelWrapperMap.get(mChannel.getId()); - if (wrapper != null) { - wrapper.mChannel.setChannelLogoExist(result); - } - } - } - private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask { public QueryAllChannelsTask(ContentResolver contentResolver) { @@ -660,8 +625,6 @@ public class ChannelDataManager { boolean newlyAdded = !removedChannelIds.remove(channelId); ChannelWrapper channelWrapper; if (newlyAdded) { - new checkChannelLogoExistTask(channel) - .executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); channelWrapper = new ChannelWrapper(channel); mChannelWrapperMap.put(channel.getId(), channelWrapper); if (!channelWrapper.mInputRemoved) { @@ -677,9 +640,9 @@ public class ChannelDataManager { // {@link #applyUpdatedValuesToDb} is called. Therefore, the value // between DB and ChannelDataManager could be different for a while. // Therefore, we'll keep the values in ChannelDataManager. + channelWrapper.mChannel.copyFrom(channel); channel.setBrowsable(oldChannel.isBrowsable()); channel.setLocked(oldChannel.isLocked()); - channelWrapper.mChannel.copyFrom(channel); if (!channelWrapper.mInputRemoved) { channelUpdated = true; updatedChannelWrappers.add(channelWrapper); @@ -730,6 +693,7 @@ public class ChannelDataManager { r.run(); } mPostRunnablesAfterChannelUpdate.clear(); + ChannelLogoFetcher.startFetchingChannelLogos(mContext); } } diff --git a/src/com/android/tv/data/ChannelLogoFetcher.java b/src/com/android/tv/data/ChannelLogoFetcher.java index 256ecdb2..5a549f83 100644 --- a/src/com/android/tv/data/ChannelLogoFetcher.java +++ b/src/com/android/tv/data/ChannelLogoFetcher.java @@ -16,68 +16,155 @@ package com.android.tv.data; -import android.content.ContentProviderOperation; import android.content.Context; -import android.content.OperationApplicationException; -import android.content.SharedPreferences; +import android.database.Cursor; import android.graphics.Bitmap.CompressFormat; import android.media.tv.TvContract; +import android.media.tv.TvContract.Channels; import android.net.Uri; import android.os.AsyncTask; -import android.os.RemoteException; -import android.support.annotation.AnyThread; +import android.support.annotation.WorkerThread; import android.text.TextUtils; import android.util.Log; -import com.android.tv.common.SharedPreferencesUtils; +import com.android.tv.util.AsyncDbTask; import com.android.tv.util.BitmapUtils; import com.android.tv.util.BitmapUtils.ScaledBitmapInfo; import com.android.tv.util.PermissionUtils; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; import java.io.OutputStream; import java.util.ArrayList; -import java.util.Map; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; /** - * Fetches channel logos from the cloud into the database. It's for the channels which have no logos - * or need update logos. This class is thread safe. + * Utility class for TMS data. + * This class is thread safe. */ public class ChannelLogoFetcher { private static final String TAG = "ChannelLogoFetcher"; private static final boolean DEBUG = false; - private static final String PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO = - "is_first_time_fetch_channel_logo"; + /** + * The name of the file which contains the TMS data. + * The file has multiple records and each of them is a string separated by '|' like + * STATION_NAME|SHORT_NAME|CALL_SIGN|LOGO_URI. + */ + private static final String TMS_US_TABLE_FILE = "tms_us.table"; + private static final String TMS_KR_TABLE_FILE = "tms_kr.table"; + private static final String FIELD_SEPARATOR = "\\|"; + private static final String NAME_SEPARATOR_FOR_TMS = "\\(|\\)|\\{|\\}|\\[|\\]"; + private static final String NAME_SEPARATOR_FOR_DB = "\\W"; + private static final int INDEX_NAME = 0; + private static final int INDEX_SHORT_NAME = 1; + private static final int INDEX_CALL_SIGN = 2; + private static final int INDEX_LOGO_URI = 3; + + private static final String COLUMN_CHANNEL_LOGO = "logo"; + private static final Object sLock = new Object(); + private static final Set<Long> sChannelIdBlackListSet = new HashSet<>(); + private static LoadChannelTask sQueryTask; private static FetchLogoTask sFetchTask; /** - * Fetches the channel logos from the cloud data and insert them into TvProvider. + * Fetch the channel logos from TMS data and insert them into TvProvider. * The previous task is canceled and a new task starts. */ - @AnyThread - public static synchronized void startFetchingChannelLogos( - Context context, List<Channel> channels) { + public static void startFetchingChannelLogos(Context context) { if (!PermissionUtils.hasAccessAllEpg(context)) { // TODO: support this feature for non-system LC app. b/23939816 return; } - if (sFetchTask != null) { - sFetchTask.cancel(true); + synchronized (sLock) { + stopFetchingChannelLogos(); + if (DEBUG) Log.d(TAG, "Request to start fetching logos."); + sQueryTask = new LoadChannelTask(context); + sQueryTask.executeOnDbThread(); } - if (DEBUG) Log.d(TAG, "Request to start fetching logos."); - if (channels == null || channels.isEmpty()) { - return; + } + + /** + * Stops the current fetching tasks. This can be called when the Activity pauses. + */ + public static void stopFetchingChannelLogos() { + synchronized (sLock) { + if (DEBUG) Log.d(TAG, "Request to stop fetching logos."); + if (sQueryTask != null) { + sQueryTask.cancel(true); + sQueryTask = null; + } + if (sFetchTask != null) { + sFetchTask.cancel(true); + sFetchTask = null; + } } - sFetchTask = new FetchLogoTask(context, channels); - sFetchTask.execute(); } private ChannelLogoFetcher() { } + private static final class LoadChannelTask extends AsyncDbTask<Void, Void, List<Channel>> { + private final Context mContext; + + public LoadChannelTask(Context context) { + mContext = context; + } + + @Override + protected List<Channel> doInBackground(Void... arg) { + // Load channels which doesn't have channel logos. + if (DEBUG) Log.d(TAG, "Starts loading the channels from DB"); + String[] projection = + new String[] { Channels._ID, Channels.COLUMN_DISPLAY_NAME }; + String selection = COLUMN_CHANNEL_LOGO + " IS NULL AND " + + Channels.COLUMN_PACKAGE_NAME + "=?"; + String[] selectionArgs = new String[] { mContext.getPackageName() }; + try (Cursor c = mContext.getContentResolver().query(Channels.CONTENT_URI, + projection, selection, selectionArgs, null)) { + if (c == null) { + Log.e(TAG, "Query returns null cursor", new RuntimeException()); + return null; + } + List<Channel> channels = new ArrayList<>(); + while (!isCancelled() && c.moveToNext()) { + long channelId = c.getLong(0); + if (sChannelIdBlackListSet.contains(channelId)) { + continue; + } + channels.add(new Channel.Builder().setId(c.getLong(0)) + .setDisplayName(c.getString(1).toUpperCase(Locale.getDefault())) + .build()); + } + return channels; + } + } + + @Override + protected void onPostExecute(List<Channel> channels) { + synchronized (sLock) { + if (DEBUG) { + int count = channels == null ? 0 : channels.size(); + Log.d(TAG, count + " channels are loaded"); + } + if (sQueryTask == this) { + sQueryTask = null; + if (channels != null && !channels.isEmpty()) { + sFetchTask = new FetchLogoTask(mContext, channels); + sFetchTask.execute(); + } + } + } + } + } + private static final class FetchLogoTask extends AsyncTask<Void, Void, Void> { private final Context mContext; private final List<Channel> mChannels; @@ -93,53 +180,83 @@ public class ChannelLogoFetcher { if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled"); return null; } - List<Channel> channelsToUpdate = new ArrayList<>(); - List<Channel> channelsToRemove = new ArrayList<>(); - // Updates or removes the logo by comparing the logo uri which is got from the cloud - // and the stored one. And we assume that the data got form the cloud is 100% - // correct and completed. - SharedPreferences sharedPreferences = - mContext.getSharedPreferences( - SharedPreferencesUtils.SHARED_PREF_CHANNEL_LOGO_URIS, - Context.MODE_PRIVATE); - SharedPreferences.Editor sharedPreferencesEditor = sharedPreferences.edit(); - Map<String, ?> uncheckedChannels = sharedPreferences.getAll(); - boolean isFirstTimeFetchChannelLogo = sharedPreferences.getBoolean( - PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, true); - // Iterating channels. - for (Channel channel : mChannels) { - String channelIdString = Long.toString(channel.getId()); - String storedChannelLogoUri = (String) uncheckedChannels.remove(channelIdString); - if (!TextUtils.isEmpty(channel.getLogoUri()) - && !TextUtils.equals(storedChannelLogoUri, channel.getLogoUri())) { - channelsToUpdate.add(channel); - sharedPreferencesEditor.putString(channelIdString, channel.getLogoUri()); - } else if (TextUtils.isEmpty(channel.getLogoUri()) - && (!TextUtils.isEmpty(storedChannelLogoUri) - || isFirstTimeFetchChannelLogo)) { - channelsToRemove.add(channel); - sharedPreferencesEditor.remove(channelIdString); + // Load the TMS table data. + if (DEBUG) Log.d(TAG, "Loads TMS data"); + Map<String, String> channelNameLogoUriMap = new HashMap<>(); + try { + channelNameLogoUriMap.putAll(readTmsFile(mContext, TMS_US_TABLE_FILE)); + if (isCancelled()) { + if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled"); + return null; } + channelNameLogoUriMap.putAll(readTmsFile(mContext, TMS_KR_TABLE_FILE)); + } catch (IOException e) { + Log.e(TAG, "Loading TMS data failed.", e); + return null; } - - // Removes non existing channels from SharedPreferences. - for (String channelId : uncheckedChannels.keySet()) { - sharedPreferencesEditor.remove(channelId); + if (isCancelled()) { + if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled"); + return null; } - // Updates channel logos. - for (Channel channel : channelsToUpdate) { + // Iterating channels. + for (Channel channel : mChannels) { if (isCancelled()) { if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled"); return null; } - // Downloads the channel logo. - String logoUri = channel.getLogoUri(); + // Download the channel logo. + if (TextUtils.isEmpty(channel.getDisplayName())) { + if (DEBUG) { + Log.d(TAG, "The channel with ID (" + channel.getId() + + ") doesn't have the display name."); + } + sChannelIdBlackListSet.add(channel.getId()); + continue; + } + String channelName = channel.getDisplayName().trim(); + String logoUri = channelNameLogoUriMap.get(channelName); + if (TextUtils.isEmpty(logoUri)) { + if (DEBUG) { + Log.d(TAG, "Can't find a logo URI for channel '" + channelName + "'"); + } + // Find the candidate names. If the channel name is CNN-HD, then find CNNHD + // and CNN. Or if the channel name is KQED+, then find KQED. + String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_DB); + if (splitNames.length > 1) { + StringBuilder sb = new StringBuilder(); + for (String splitName : splitNames) { + sb.append(splitName); + } + logoUri = channelNameLogoUriMap.get(sb.toString()); + if (DEBUG) { + if (TextUtils.isEmpty(logoUri)) { + Log.d(TAG, "Can't find a logo URI for channel '" + sb.toString() + + "'"); + } + } + } + if (TextUtils.isEmpty(logoUri) + && splitNames[0].length() != channelName.length()) { + logoUri = channelNameLogoUriMap.get(splitNames[0]); + if (DEBUG) { + if (TextUtils.isEmpty(logoUri)) { + Log.d(TAG, "Can't find a logo URI for channel '" + splitNames[0] + + "'"); + } + } + } + } + if (TextUtils.isEmpty(logoUri)) { + sChannelIdBlackListSet.add(channel.getId()); + continue; + } ScaledBitmapInfo bitmapInfo = BitmapUtils.decodeSampledBitmapFromUriString( mContext, logoUri, Integer.MAX_VALUE, Integer.MAX_VALUE); if (bitmapInfo == null) { Log.e(TAG, "Failed to load bitmap. {channelName=" + channel.getDisplayName() + ", " + "logoUri=" + logoUri + "}"); + sChannelIdBlackListSet.add(channel.getId()); continue; } if (isCancelled()) { @@ -147,15 +264,12 @@ public class ChannelLogoFetcher { return null; } - // Inserts the logo to DB. + // Insert the logo to DB. Uri dstLogoUri = TvContract.buildChannelLogoUri(channel.getId()); try (OutputStream os = mContext.getContentResolver().openOutputStream(dstLogoUri)) { bitmapInfo.bitmap.compress(CompressFormat.PNG, 100, os); } catch (IOException e) { Log.e(TAG, "Failed to write " + logoUri + " to " + dstLogoUri, e); - // Removes it from the shared preference for the failed channels to make it - // retry next time. - sharedPreferencesEditor.remove(Long.toString(channel.getId())); continue; } if (DEBUG) { @@ -163,30 +277,63 @@ public class ChannelLogoFetcher { + dstLogoUri + "}"); } } + if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully."); + return null; + } - // Removes the logos for the channels that have logos before but now - // their logo uris are null. - boolean deleteChannelLogoFailed = false; - if (!channelsToRemove.isEmpty()) { - ArrayList<ContentProviderOperation> ops = new ArrayList<>(); - for (Channel channel : channelsToRemove) { - ops.add(ContentProviderOperation.newDelete( - TvContract.buildChannelLogoUri(channel.getId())).build()); - } - try { - mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops); - } catch (RemoteException | OperationApplicationException e) { - deleteChannelLogoFailed = true; - Log.e(TAG, "Error deleting obsolete channels", e); + @WorkerThread + private Map<String, String> readTmsFile(Context context, String fileName) + throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + context.getAssets().open(fileName)))) { + Map<String, String> channelNameLogoUriMap = new HashMap<>(); + String line; + while ((line = reader.readLine()) != null && !isCancelled()) { + String[] data = line.split(FIELD_SEPARATOR); + if (data.length != INDEX_LOGO_URI + 1) { + if (DEBUG) Log.d(TAG, "Invalid or comment row: " + line); + continue; + } + addChannelNames(channelNameLogoUriMap, + data[INDEX_NAME].toUpperCase(Locale.getDefault()), + data[INDEX_LOGO_URI]); + addChannelNames(channelNameLogoUriMap, + data[INDEX_SHORT_NAME].toUpperCase(Locale.getDefault()), + data[INDEX_LOGO_URI]); + addChannelNames(channelNameLogoUriMap, + data[INDEX_CALL_SIGN].toUpperCase(Locale.getDefault()), + data[INDEX_LOGO_URI]); } + return channelNameLogoUriMap; } - if (isFirstTimeFetchChannelLogo && !deleteChannelLogoFailed) { - sharedPreferencesEditor.putBoolean( - PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, false); + } + + private void addChannelNames(Map<String, String> channelNameLogoUriMap, String channelName, + String logoUri) { + if (!TextUtils.isEmpty(channelName)) { + channelNameLogoUriMap.put(channelName, logoUri); + // Find the candidate names. + // If the name is like "W05AAD (W05AA-D)", then split the names into "W05AAD" and + // "W05AA-D" + String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_TMS); + if (splitNames.length > 1) { + for (String name : splitNames) { + name = name.trim(); + if (channelNameLogoUriMap.get(name) == null) { + channelNameLogoUriMap.put(name, logoUri); + } + } + } + } + } + + @Override + protected void onPostExecute(Void result) { + synchronized (sLock) { + if (sFetchTask == this) { + sFetchTask = null; + } } - sharedPreferencesEditor.commit(); - if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully."); - return null; } } } diff --git a/src/com/android/tv/data/ChannelNumber.java b/src/com/android/tv/data/ChannelNumber.java index 29054aa5..59021609 100644 --- a/src/com/android/tv/data/ChannelNumber.java +++ b/src/com/android/tv/data/ChannelNumber.java @@ -17,38 +17,37 @@ package com.android.tv.data; import android.support.annotation.NonNull; -import android.text.TextUtils; import android.view.KeyEvent; -import com.android.tv.util.StringUtils; - -import java.util.Objects; - /** * A convenience class to handle channel number. */ public final class ChannelNumber implements Comparable<ChannelNumber> { + public static final String PRIMARY_CHANNEL_DELIMITER = "-"; + public static final String[] CHANNEL_DELIMITERS = {"-", ".", " "}; + private static final int[] CHANNEL_DELIMITER_KEYCODES = { KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_NUMPAD_SUBTRACT, KeyEvent.KEYCODE_PERIOD, KeyEvent.KEYCODE_NUMPAD_DOT, KeyEvent.KEYCODE_SPACE }; - /** The major part of the channel number. */ public String majorNumber; - /** The flag which indicates whether it has a delimiter or not. */ public boolean hasDelimiter; - /** The major part of the channel number. */ public String minorNumber; public ChannelNumber() { reset(); } + public ChannelNumber(String major, boolean hasDelimiter, String minor) { + setChannelNumber(major, hasDelimiter, minor); + } + public void reset() { setChannelNumber("", false, ""); } - private void setChannelNumber(String majorNumber, boolean hasDelimiter, String minorNumber) { + public void setChannelNumber(String majorNumber, boolean hasDelimiter, String minorNumber) { this.majorNumber = majorNumber; this.hasDelimiter = hasDelimiter; this.minorNumber = minorNumber; @@ -57,7 +56,7 @@ public final class ChannelNumber implements Comparable<ChannelNumber> { @Override public String toString() { if (hasDelimiter) { - return majorNumber + Channel.CHANNEL_NUMBER_DELIMITER + minorNumber; + return majorNumber + PRIMARY_CHANNEL_DELIMITER + minorNumber; } return majorNumber; } @@ -76,22 +75,6 @@ public final class ChannelNumber implements Comparable<ChannelNumber> { return major - opponentMajor; } - @Override - public boolean equals(Object obj) { - if (obj instanceof ChannelNumber) { - ChannelNumber channelNumber = (ChannelNumber) obj; - return TextUtils.equals(majorNumber, channelNumber.majorNumber) - && TextUtils.equals(minorNumber, channelNumber.minorNumber) - && hasDelimiter == channelNumber.hasDelimiter; - } - return super.equals(obj); - } - - @Override - public int hashCode() { - return Objects.hash(majorNumber, hasDelimiter, minorNumber); - } - public static boolean isChannelNumberDelimiterKey(int keyCode) { for (int delimiterKeyCode : CHANNEL_DELIMITER_KEYCODES) { if (delimiterKeyCode == keyCode) { @@ -101,22 +84,22 @@ public final class ChannelNumber implements Comparable<ChannelNumber> { return false; } - /** - * Returns the ChannelNumber instance. - * <p> - * Note that all the channel number argument should be normalized by - * {@link Channel#normalizeDisplayNumber}. The channels retrieved from - * {@link ChannelDataManager} are already normalized. - */ public static ChannelNumber parseChannelNumber(String number) { if (number == null) { return null; } ChannelNumber ret = new ChannelNumber(); - int indexOfDelimiter = number.indexOf(Channel.CHANNEL_NUMBER_DELIMITER); + int indexOfDelimiter = -1; + for (String delimiter : CHANNEL_DELIMITERS) { + indexOfDelimiter = number.indexOf(delimiter); + if (indexOfDelimiter >= 0) { + break; + } + } if (indexOfDelimiter == 0 || indexOfDelimiter == number.length() - 1) { return null; - } else if (indexOfDelimiter < 0) { + } + if (indexOfDelimiter < 0) { ret.majorNumber = number; if (!isInteger(ret.majorNumber)) { return null; @@ -132,31 +115,25 @@ public final class ChannelNumber implements Comparable<ChannelNumber> { return ret; } - /** - * Compares the channel numbers. - * <p> - * Note that all the channel number arguments should be normalized by - * {@link Channel#normalizeDisplayNumber}. The channels retrieved from - * {@link ChannelDataManager} are already normalized. - */ public static int compare(String lhs, String rhs) { ChannelNumber lhsNumber = parseChannelNumber(lhs); ChannelNumber rhsNumber = parseChannelNumber(rhs); - // Null first if (lhsNumber == null && rhsNumber == null) { - return StringUtils.compare(lhs, rhs); + return 0; } else if (lhsNumber == null /* && rhsNumber != null */) { return -1; - } else if (rhsNumber == null) { + } else if (lhsNumber != null && rhsNumber == null) { return 1; } return lhsNumber.compareTo(rhsNumber); } - private static boolean isInteger(String string) { + public static boolean isInteger(String string) { try { Integer.parseInt(string); - } catch(NumberFormatException | NullPointerException e) { + } catch(NumberFormatException e) { + return false; + } catch(NullPointerException e) { return false; } return true; diff --git a/src/com/android/tv/data/InternalDataUtils.java b/src/com/android/tv/data/InternalDataUtils.java index e33ca18f..6054f089 100644 --- a/src/com/android/tv/data/InternalDataUtils.java +++ b/src/com/android/tv/data/InternalDataUtils.java @@ -21,7 +21,7 @@ import android.text.TextUtils; import android.util.Log; import com.android.tv.data.Program.CriticScore; -import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.RecordedProgram; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; diff --git a/src/com/android/tv/data/StreamInfo.java b/src/com/android/tv/data/StreamInfo.java index 709863cf..fe461f14 100644 --- a/src/com/android/tv/data/StreamInfo.java +++ b/src/com/android/tv/data/StreamInfo.java @@ -38,9 +38,5 @@ public interface StreamInfo { int getAudioChannelCount(); boolean hasClosedCaption(); boolean isVideoAvailable(); - /** - * Returns true, if video or audio is available. - */ - boolean isVideoOrAudioAvailable(); int getVideoUnavailableReason(); } diff --git a/src/com/android/tv/data/epg/EpgFetcher.java b/src/com/android/tv/data/epg/EpgFetcher.java index ddd68ad7..3b093b6a 100644 --- a/src/com/android/tv/data/epg/EpgFetcher.java +++ b/src/com/android/tv/data/epg/EpgFetcher.java @@ -16,11 +16,13 @@ package com.android.tv.data.epg; +import android.Manifest; import android.annotation.SuppressLint; import android.content.ContentProviderOperation; import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; +import android.content.pm.PackageManager; import android.database.Cursor; import android.location.Address; import android.media.tv.TvContentRating; @@ -44,11 +46,9 @@ 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.ChannelLogoFetcher; import com.android.tv.data.InternalDataUtils; import com.android.tv.data.Lineup; import com.android.tv.data.Program; -import com.android.tv.tuner.util.PostalCodeUtils; import com.android.tv.util.LocationUtils; import com.android.tv.util.RecurringRunner; import com.android.tv.util.Utils; @@ -56,10 +56,8 @@ import com.android.tv.util.Utils; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -71,27 +69,14 @@ public class EpgFetcher { private static final boolean DEBUG = false; private static final int MSG_FETCH_EPG = 1; - private static final int MSG_FAST_FETCH_EPG = 2; 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 LOCATION_INIT_WAIT_MS = TimeUnit.SECONDS.toMillis(10); private static final long LOCATION_ERROR_WAIT_MS = TimeUnit.HOURS.toMillis(1); - private static final long NO_INFO_FETCHED_WAIT_MS = TimeUnit.SECONDS.toMillis(10); private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(30); - private static final long PROGRAM_FETCH_SHORT_DURATION_SEC = TimeUnit.HOURS.toSeconds(3); - private static final long PROGRAM_FETCH_LONG_DURATION_SEC = TimeUnit.DAYS.toSeconds(2) - + EPG_PREFETCH_RECURRING_PERIOD_MS / 1000; - - // This equals log2(EPG_PREFETCH_RECURRING_PERIOD_MS / NO_INFO_FETCHED_WAIT_MS + 1), - // since we will double waiting time every other trial, therefore this limit the maximum - // waiting time less than half of EPG_PREFETCH_RECURRING_PERIOD_MS. - private static final int NO_INFO_RETRY_LIMIT = 31 - Integer.numberOfLeadingZeros( - (int) (EPG_PREFETCH_RECURRING_PERIOD_MS / NO_INFO_FETCHED_WAIT_MS + 1)); - private static final int BATCH_OPERATION_COUNT = 100; - private static final int QUERY_CHANNEL_COUNT = 50; private static final String SUPPORTED_COUNTRY_CODE = Locale.US.getCountry(); private static final String CONTENT_RATING_SEPARATOR = ","; @@ -111,11 +96,8 @@ public class EpgFetcher { private EpgFetcherHandler mHandler; private RecurringRunner mRecurringRunner; private boolean mStarted; - private boolean mScanningChannels; - private int mFetchRetryCount; private long mLastEpgTimestamp = -1; - // @GuardedBy("this") private String mLineupId; public static synchronized EpgFetcher getInstance(Context context) { @@ -140,33 +122,21 @@ public class EpgFetcher { @Override public void onLoadFinished() { if (DEBUG) Log.d(TAG, "ChannelDataManager.onLoadFinished()"); - if (!mScanningChannels) { - handleChannelChanged(); - } + handleChannelChanged(); } @Override public void onChannelListUpdated() { if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelListUpdated()"); - if (!mScanningChannels) { - handleChannelChanged(); - } + handleChannelChanged(); } @Override public void onChannelBrowsableChanged() { if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelBrowsableChanged()"); - if (!mScanningChannels) { - handleChannelChanged(); - } + handleChannelChanged(); } }); - // Warm up to get address, because the first call of getCurrentAddress is usually failed. - try { - LocationUtils.getCurrentAddress(mContext); - } catch (SecurityException | IOException e) { - // Do nothing - } } private void handleChannelChanged() { @@ -175,9 +145,7 @@ public class EpgFetcher { stop(); } } else { - if (canStart()) { - start(); - } + start(); } } @@ -205,14 +173,17 @@ public class EpgFetcher { if (!TextUtils.isEmpty(getLastLineupId())) { return true; } - if (!TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { - return true; + if (mContext.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + if (DEBUG) Log.d(TAG, "No permission to check the current location."); + return false; } + try { Address address = LocationUtils.getCurrentAddress(mContext); if (address != null && !TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) { - Log.i(TAG, "Country not supported: " + address.getCountryCode()); + if (DEBUG) Log.d(TAG, "Country not supported: " + address.getCountryCode()); return false; } } catch (SecurityException e) { @@ -226,13 +197,9 @@ public class EpgFetcher { /** * Starts fetching EPG. - * - * @param resetNextRunTime if true, next run time is reset, so EPG will be fetched - * {@link #EPG_PREFETCH_RECURRING_PERIOD_MS} later. - * otherwise, EPG is fetched when this method is called. */ @MainThread - private void startInternal(boolean resetNextRunTime) { + public void start() { if (DEBUG) Log.d(TAG, "start()"); if (mStarted) { if (DEBUG) Log.d(TAG, "EpgFetcher thread already started."); @@ -248,35 +215,19 @@ public class EpgFetcher { mHandler = new EpgFetcherHandler(handlerThread.getLooper(), this); mRecurringRunner = new RecurringRunner(mContext, EPG_PREFETCH_RECURRING_PERIOD_MS, new EpgRunner(), null); - mRecurringRunner.start(resetNextRunTime); + mRecurringRunner.start(); if (DEBUG) Log.d(TAG, "EpgFetcher thread started successfully."); } - @MainThread - public void start() { - if (System.currentTimeMillis() - getLastUpdatedEpgTimestamp() > - EPG_PREFETCH_RECURRING_PERIOD_MS) { - startImmediately(false); - } else { - startInternal(false); - } - } - /** * Starts fetching EPG immediately if possible without waiting for the timer. - * - * @param clearStoredLineupId if true, stored lineup id will be clear before fetching EPG. */ @MainThread - public void startImmediately(boolean clearStoredLineupId) { - startInternal(true); + public void startImmediately() { + start(); if (mStarted) { - if (clearStoredLineupId) { - if (DEBUG) Log.d(TAG, "Clear stored lineup id: " + mLineupId); - setLastLineupId(null); - } if (DEBUG) Log.d(TAG, "Starting fetcher immediately"); - postFetchRequest(true, 0); + fetchEpg(); } } @@ -295,71 +246,48 @@ public class EpgFetcher { mHandler.getLooper().quit(); } - /** - * Notifies EPG fetcher that channel scanning is started. - */ - @MainThread - public void onChannelScanStarted() { - stop(); - mScanningChannels = true; + private void fetchEpg() { + fetchEpg(0); } - /** - * Notifies EPG fetcher that channel scanning is finished. - */ - @MainThread - public void onChannelScanFinished() { - mScanningChannels = false; - start(); - } - - private void postFetchRequest(boolean fastFetch, long delay) { - int msg = fastFetch ? MSG_FAST_FETCH_EPG : MSG_FETCH_EPG; - mHandler.removeMessages(msg); - mHandler.sendEmptyMessageDelayed(msg, delay); + private void fetchEpg(long delay) { + mHandler.removeMessages(MSG_FETCH_EPG); + mHandler.sendEmptyMessageDelayed(MSG_FETCH_EPG, delay); } private void onFetchEpg() { - onFetchEpg(false); - } - - private void onFetchEpg(boolean fastFetch) { if (DEBUG) Log.d(TAG, "Start fetching EPG."); if (!mEpgReader.isAvailable()) { - Log.i(TAG, "EPG reader is not temporarily available."); - postFetchRequest(fastFetch, EPG_READER_INIT_WAIT_MS); + if (DEBUG) Log.d(TAG, "EPG reader is not temporarily available."); + fetchEpg(EPG_READER_INIT_WAIT_MS); return; } String lineupId = getLastLineupId(); if (lineupId == null) { + Address address; try { - PostalCodeUtils.updatePostalCode(mContext); + address = LocationUtils.getCurrentAddress(mContext); } catch (IOException e) { - if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { - if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e); - postFetchRequest(fastFetch, LOCATION_ERROR_WAIT_MS); - return; - } + if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e); + fetchEpg(LOCATION_ERROR_WAIT_MS); + return; } catch (SecurityException e) { - if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { - Log.w(TAG, "No permission to get the current location."); - return; - } - } catch (PostalCodeUtils.NoPostalCodeException e) { - Log.i(TAG, "Failed to get the current postal code."); - postFetchRequest(fastFetch, LOCATION_INIT_WAIT_MS); + Log.w(TAG, "No permission to get the current location."); return; } - String postalCode = PostalCodeUtils.getLastPostalCode(mContext); - if (DEBUG) Log.d(TAG, "The current postal code is " + postalCode); + if (address == null) { + if (DEBUG) Log.d(TAG, "Null address returned."); + fetchEpg(LOCATION_INIT_WAIT_MS); + return; + } + if (DEBUG) Log.d(TAG, "Current location is " + address); - lineupId = pickLineupForPostalCode(postalCode); + lineupId = getLineupForAddress(address); if (lineupId != null) { - Log.i(TAG, "Selecting the lineup " + lineupId); + if (DEBUG) Log.d(TAG, "Saving lineup " + lineupId + "found for " + address); setLastLineupId(lineupId); } else { - Log.i(TAG, "Failed to get lineup id"); - retryFetchEpg(fastFetch); + if (DEBUG) Log.d(TAG, "No lineup found for " + address); return; } } @@ -371,109 +299,48 @@ public class EpgFetcher { return; } + boolean updated = false; List<Channel> channels = mEpgReader.getChannels(lineupId); - if (channels.isEmpty()) { - Log.i(TAG, "Failed to get EPG channels."); - retryFetchEpg(fastFetch); - return; - } - mFetchRetryCount = 0; - if (!fastFetch) { - for (Channel channel : channels) { - if (!mStarted) { - break; - } - List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(channel.getId())); - Collections.sort(programs); - Log.i(TAG, "Fetched " + programs.size() + " programs for channel " + channel); - updateEpg(channel.getId(), programs); + for (Channel channel : channels) { + List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(channel.getId())); + Collections.sort(programs); + if (DEBUG) { + Log.d(TAG, "Fetched " + programs.size() + " programs for channel " + channel); + } + if (updateEpg(channel.getId(), programs)) { + updated = true; } - setLastUpdatedEpgTimestamp(epgTimestamp); - } else { - handleFastFetch(channels, PROGRAM_FETCH_SHORT_DURATION_SEC); - if (DEBUG) Log.d(TAG, "First fast fetch Done."); - handleFastFetch(channels, PROGRAM_FETCH_LONG_DURATION_SEC); - if (DEBUG) Log.d(TAG, "Second fast fetch Done."); } - if (!fastFetch) { - mHandler.removeMessages(MSG_FETCH_EPG); - } + final boolean epgUpdated = updated; + setLastUpdatedEpgTimestamp(epgTimestamp); + mHandler.removeMessages(MSG_FETCH_EPG); if (DEBUG) Log.d(TAG, "Fetching EPG is finished."); - // Start to fetch channel logos after epg fetching finished. - ChannelLogoFetcher.startFetchingChannelLogos(mContext, channels); - } - - private void retryFetchEpg(boolean fastFetch) { - if (mFetchRetryCount < NO_INFO_RETRY_LIMIT) { - postFetchRequest(fastFetch, NO_INFO_FETCHED_WAIT_MS * 1 << mFetchRetryCount); - mFetchRetryCount++; - } else { - mFetchRetryCount = 0; - } } - private void handleFastFetch(List<Channel> channels, long duration) { - List<Long> channelIds = new ArrayList<>(channels.size()); - for (Channel channel : channels) { - channelIds.add(channel.getId()); - } - Map<Long, List<Program>> allPrograms = new HashMap<>(); - List<Long> queryChannelIds = new ArrayList<>(QUERY_CHANNEL_COUNT); - for (Long channelId : channelIds) { - queryChannelIds.add(channelId); - if (queryChannelIds.size() >= QUERY_CHANNEL_COUNT) { - allPrograms.putAll( - new HashMap<>(mEpgReader.getPrograms(queryChannelIds, duration))); - queryChannelIds.clear(); + @Nullable + private String getLineupForAddress(Address address) { + String lineup = null; + if (TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) { + String postalCode = address.getPostalCode(); + if (!TextUtils.isEmpty(postalCode)) { + lineup = getLineupForPostalCode(postalCode); } } - if (!queryChannelIds.isEmpty()) { - allPrograms.putAll( - new HashMap<>(mEpgReader.getPrograms(queryChannelIds, duration))); - } - for (Channel channel : channels) { - List<Program> programs = allPrograms.get(channel.getId()); - if (programs == null) continue; - Collections.sort(programs); - Log.i(TAG, "Fast fetched " + programs.size() + " programs for channel " + channel); - updateEpg(channel.getId(), programs); - } + return lineup; } @Nullable - private String pickLineupForPostalCode(String postalCode) { + private String getLineupForPostalCode(String postalCode) { List<Lineup> lineups = mEpgReader.getLineups(postalCode); - int maxCount = 0; - String maxLineupId = null; for (Lineup lineup : lineups) { - int count = getMatchedChannelCount(lineup.id); - Log.i(TAG, lineup.name + " (" + lineup.id + ") - " + count + " matches"); - if (count > maxCount) { - maxCount = count; - maxLineupId = lineup.id; + // TODO(EPG): handle more than OTA digital + if (lineup.type == Lineup.LINEUP_BROADCAST_DIGITAL) { + if (DEBUG) Log.d(TAG, "Setting lineup to " + lineup.name + "(" + lineup.id + ")"); + return lineup.id; } } - return maxLineupId; - } - - private int getMatchedChannelCount(String lineupId) { - // Construct a list of display numbers for existing channels. - List<Channel> channels = mChannelDataManager.getChannelList(); - if (channels.isEmpty()) { - if (DEBUG) Log.d(TAG, "No existing channel to compare"); - return 0; - } - List<String> numbers = new ArrayList<>(channels.size()); - for (Channel c : channels) { - // We only support local channels from physical tuners. - if (c.isPhysicalTunerChannel()) { - numbers.add(c.getDisplayNumber()); - } - } - - numbers.retainAll(mEpgReader.getChannelNumbers(lineupId)); - return numbers.size(); + return null; } private long getLastUpdatedEpgTimestamp() { @@ -490,16 +357,16 @@ public class EpgFetcher { KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp).commit(); } - synchronized private String getLastLineupId() { + private String getLastLineupId() { if (mLineupId == null) { mLineupId = PreferenceManager.getDefaultSharedPreferences(mContext) .getString(KEY_LAST_LINEUP_ID, null); } - if (DEBUG) Log.d(TAG, "Last lineup is " + mLineupId); + if (DEBUG) Log.d(TAG, "Last lineup_id " + mLineupId); return mLineupId; } - synchronized private void setLastLineupId(String lineupId) { + private void setLastLineupId(String lineupId) { mLineupId = lineupId; PreferenceManager.getDefaultSharedPreferences(mContext).edit() .putString(KEY_LAST_LINEUP_ID, lineupId).commit(); @@ -514,9 +381,19 @@ public class EpgFetcher { long startTimeMs = System.currentTimeMillis(); long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION; List<Program> oldPrograms = queryPrograms(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<>(); @@ -562,7 +439,7 @@ public class EpgFetcher { } if (addNewProgram) { ops.add(ContentProviderOperation - .newInsert(Programs.CONTENT_URI) + .newInsert(TvContract.Programs.CONTENT_URI) .withValues(toContentValues(newProgram)) .build()); } @@ -624,25 +501,27 @@ public class EpgFetcher { @SuppressWarnings("deprecation") private static ContentValues toContentValues(Program program) { ContentValues values = new ContentValues(); - values.put(Programs.COLUMN_CHANNEL_ID, program.getChannelId()); - putValue(values, Programs.COLUMN_TITLE, program.getTitle()); - putValue(values, Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle()); + 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()); if (BuildCompat.isAtLeastN()) { - putValue(values, Programs.COLUMN_SEASON_DISPLAY_NUMBER, program.getSeasonNumber()); - putValue(values, Programs.COLUMN_EPISODE_DISPLAY_NUMBER, program.getEpisodeNumber()); + putValue(values, TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, + program.getSeasonNumber()); + putValue(values, TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER, + program.getEpisodeNumber()); } else { - putValue(values, Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber()); - putValue(values, Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber()); + putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber()); + putValue(values, TvContract.Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber()); } - putValue(values, Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription()); - putValue(values, Programs.COLUMN_LONG_DESCRIPTION, program.getLongDescription()); - putValue(values, Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri()); - putValue(values, Programs.COLUMN_THUMBNAIL_URI, program.getThumbnailUri()); + putValue(values, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription()); + putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri()); + putValue(values, TvContract.Programs.COLUMN_THUMBNAIL_URI, program.getThumbnailUri()); String[] canonicalGenres = program.getCanonicalGenres(); if (canonicalGenres != null && canonicalGenres.length > 0) { - putValue(values, Programs.COLUMN_CANONICAL_GENRE, Genres.encode(canonicalGenres)); + putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, + Genres.encode(canonicalGenres)); } else { - putValue(values, Programs.COLUMN_CANONICAL_GENRE, ""); + putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, ""); } TvContentRating[] ratings = program.getContentRatings(); if (ratings != null && ratings.length > 0) { @@ -651,13 +530,14 @@ public class EpgFetcher { sb.append(CONTENT_RATING_SEPARATOR); sb.append(ratings[i].flattenToString()); } - putValue(values, Programs.COLUMN_CONTENT_RATING, sb.toString()); + putValue(values, TvContract.Programs.COLUMN_CONTENT_RATING, sb.toString()); } else { - putValue(values, Programs.COLUMN_CONTENT_RATING, ""); + putValue(values, TvContract.Programs.COLUMN_CONTENT_RATING, ""); } - values.put(Programs.COLUMN_START_TIME_UTC_MILLIS, program.getStartTimeUtcMillis()); - values.put(Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis()); - putValue(values, Programs.COLUMN_INTERNAL_PROVIDER_DATA, + values.put(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, + program.getStartTimeUtcMillis()); + values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis()); + putValue(values, TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA, InternalDataUtils.serializeInternalProviderData(program)); return values; } @@ -689,9 +569,6 @@ public class EpgFetcher { case MSG_FETCH_EPG: epgFetcher.onFetchEpg(); break; - case MSG_FAST_FETCH_EPG: - epgFetcher.onFetchEpg(true); - break; default: super.handleMessage(msg); break; @@ -702,7 +579,7 @@ public class EpgFetcher { private class EpgRunner implements Runnable { @Override public void run() { - postFetchRequest(false, 0); + fetchEpg(); } } } diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java index 95cd933e..4f3b6f52 100644 --- a/src/com/android/tv/data/epg/EpgReader.java +++ b/src/com/android/tv/data/epg/EpgReader.java @@ -22,10 +22,9 @@ import android.support.annotation.WorkerThread; import com.android.tv.data.Channel; import com.android.tv.data.Lineup; import com.android.tv.data.Program; -import com.android.tv.dvr.data.SeriesInfo; +import com.android.tv.dvr.SeriesInfo; import java.util.List; -import java.util.Map; /** * An interface used to retrieve the EPG data. This class should be used in worker thread. @@ -44,37 +43,21 @@ public interface EpgReader { long getEpgTimestamp(); /** - * Returns the lineups list. - */ - List<Lineup> getLineups(@NonNull String postalCode); - - /** - * Returns the list of channel numbers (unsorted) for the given lineup. The result is used to - * choose the most appropriate lineup among others by comparing the channel numbers of the - * existing channels on the device. - */ - List<String> getChannelNumbers(@NonNull String lineupId); - - /** - * Returns the list of channels for the given lineup. The returned channels should map into the - * existing channels on the device. This method is usually called after selecting the lineup. + * Returns the channels list. */ List<Channel> getChannels(@NonNull String lineupId); /** - * Returns the programs for the given channel. Must call {@link #getChannels(String)} - * beforehand. Note that the {@code Program} doesn't have valid program ID because it's not - * retrieved from TvProvider. + * Returns the lineups list. */ - List<Program> getPrograms(long channelId); + List<Lineup> getLineups(@NonNull String postalCode); /** - * Returns the programs for the given channels. + * 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. - * This method is only used to get programs for a short duration typically. */ - Map<Long, List<Program>> getPrograms(List<Long> channelIds, long duration); + List<Program> getPrograms(long channelId); /** * Returns the series information for the given series ID. diff --git a/src/com/android/tv/data/epg/StubEpgReader.java b/src/com/android/tv/data/epg/StubEpgReader.java index 220daf22..64093f89 100644 --- a/src/com/android/tv/data/epg/StubEpgReader.java +++ b/src/com/android/tv/data/epg/StubEpgReader.java @@ -21,11 +21,10 @@ import android.content.Context; import com.android.tv.data.Channel; import com.android.tv.data.Lineup; import com.android.tv.data.Program; -import com.android.tv.dvr.data.SeriesInfo; +import com.android.tv.dvr.SeriesInfo; import java.util.Collections; import java.util.List; -import java.util.Map; /** * A stub class to read EPG. @@ -45,17 +44,12 @@ public class StubEpgReader implements EpgReader{ } @Override - public List<Lineup> getLineups(String postalCode) { - return Collections.emptyList(); - } - - @Override - public List<String> getChannelNumbers(String lineupId) { + public List<Channel> getChannels(String lineupId) { return Collections.emptyList(); } @Override - public List<Channel> getChannels(String lineupId) { + public List<Lineup> getLineups(String postalCode) { return Collections.emptyList(); } @@ -65,11 +59,6 @@ public class StubEpgReader implements EpgReader{ } @Override - public Map<Long, List<Program>> getPrograms(List<Long> channelIds, long duration) { - return Collections.emptyMap(); - } - - @Override public SeriesInfo getSeriesInfo(String seriesId) { return null; } diff --git a/src/com/android/tv/dialog/DvrHistoryDialogFragment.java b/src/com/android/tv/dialog/DvrHistoryDialogFragment.java deleted file mode 100644 index 2ed98b87..00000000 --- a/src/com/android/tv/dialog/DvrHistoryDialogFragment.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dialog; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.app.AlertDialog; -import android.app.Dialog; -import android.os.Build.VERSION_CODES; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; - -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.dvr.DvrDataManager; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.ScheduledRecording.RecordingState; -import com.android.tv.util.Utils; - -import java.util.ArrayList; -import java.util.List; - -/** - * Displays the DVR history. - */ -@TargetApi(VERSION_CODES.N) -public class DvrHistoryDialogFragment extends SafeDismissDialogFragment { - public static final String DIALOG_TAG = DvrHistoryDialogFragment.class.getSimpleName(); - - private static final String TRACKER_LABEL = "DVR history"; - private final List<ScheduledRecording> mSchedules = new ArrayList<>(); - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); - DvrDataManager dataManager = singletons.getDvrDataManager(); - ChannelDataManager channelDataManager = singletons.getChannelDataManager(); - for (ScheduledRecording schedule : dataManager.getAllScheduledRecordings()) { - if (!schedule.isInProgress() && !schedule.isNotStarted()) { - mSchedules.add(schedule); - } - } - mSchedules.sort(ScheduledRecording.START_TIME_COMPARATOR.reversed()); - LayoutInflater inflater = LayoutInflater.from(getContext()); - ArrayAdapter adapter = new ArrayAdapter<ScheduledRecording>(getContext(), - R.layout.list_item_dvr_history, ScheduledRecording.toArray(mSchedules)) { - @NonNull - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = inflater.inflate(R.layout.list_item_dvr_history, parent, false); - ScheduledRecording schedule = mSchedules.get(position); - setText(view, R.id.state, getStateString(schedule.getState())); - setText(view, R.id.schedule_time, getRecordingTimeText(schedule)); - setText(view, R.id.program_title, - schedule.getProgramTitleWithEpisodeNumber(getContext())); - setText(view, R.id.channel_name, getChannelNameText(schedule)); - return view; - } - - private void setText(View view, int id, String text) { - ((TextView) view.findViewById(id)).setText(text); - } - - private void setText(View view, int id, int text) { - ((TextView) view.findViewById(id)).setText(text); - } - - @SuppressLint("SwitchIntDef") - private int getStateString(@RecordingState int state) { - switch (state) { - case ScheduledRecording.STATE_RECORDING_CLIPPED: - return R.string.dvr_history_dialog_state_clip; - case ScheduledRecording.STATE_RECORDING_FAILED: - return R.string.dvr_history_dialog_state_fail; - case ScheduledRecording.STATE_RECORDING_FINISHED: - return R.string.dvr_history_dialog_state_success; - default: - break; - } - return 0; - } - - private String getChannelNameText(ScheduledRecording schedule) { - Channel channel = channelDataManager.getChannel(schedule.getChannelId()); - return channel == null ? null : - TextUtils.isEmpty(channel.getDisplayName()) ? channel.getDisplayNumber() : - channel.getDisplayName().trim() + " " + channel.getDisplayNumber(); - } - - private String getRecordingTimeText(ScheduledRecording schedule) { - return Utils.getDurationString(getContext(), schedule.getStartTimeMs(), - schedule.getEndTimeMs(), true, true, true, 0); - } - }; - ListView listView = new ListView(getActivity()); - listView.setAdapter(adapter); - return new AlertDialog.Builder(getActivity()).setTitle(R.string.dvr_history_dialog_title) - .setView(listView).create(); - } - - @Override - public String getTrackerLabel() { - return TRACKER_LABEL; - } -} diff --git a/src/com/android/tv/dialog/FullscreenDialogFragment.java b/src/com/android/tv/dialog/FullscreenDialogFragment.java index d00422a7..d16202a1 100644 --- a/src/com/android/tv/dialog/FullscreenDialogFragment.java +++ b/src/com/android/tv/dialog/FullscreenDialogFragment.java @@ -77,7 +77,7 @@ public class FullscreenDialogFragment extends SafeDismissDialogFragment { return mTrackerLabel; } - private class FullscreenDialog extends Dialog { + private class FullscreenDialog extends TvDialog { public FullscreenDialog(Context context, int theme) { super(context, theme); } diff --git a/src/com/android/tv/dialog/SafeDismissDialogFragment.java b/src/com/android/tv/dialog/SafeDismissDialogFragment.java index e3390b0a..f671a87d 100644 --- a/src/com/android/tv/dialog/SafeDismissDialogFragment.java +++ b/src/com/android/tv/dialog/SafeDismissDialogFragment.java @@ -17,7 +17,11 @@ package com.android.tv.dialog; import android.app.Activity; +import android.app.Dialog; import android.app.DialogFragment; +import android.content.Context; +import android.os.Bundle; +import android.view.KeyEvent; import com.android.tv.MainActivity; import com.android.tv.TvApplication; @@ -35,6 +39,11 @@ public abstract class SafeDismissDialogFragment extends DialogFragment private Tracker mTracker; @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return new TvDialog(getActivity(), getTheme()); + } + + @Override public void onAttach(Activity activity) { super.onAttach(activity); mAttached = true; @@ -83,4 +92,21 @@ public abstract class SafeDismissDialogFragment extends DialogFragment super.dismiss(); } } + + protected class TvDialog extends Dialog { + public TvDialog(Context context, int theme) { + super(context, theme); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + // When a dialog is showing, key events are handled by the dialog instead of + // MainActivity. Therefore, unless a key is a global key, it should be handled here. + if (mAttached && keyCode == KeyEvent.KEYCODE_SEARCH && mActivity != null) { + mActivity.showSearchActivity(); + return true; + } + return super.onKeyUp(keyCode, event); + } + } } diff --git a/src/com/android/tv/dialog/WebDialogFragment.java b/src/com/android/tv/dialog/WebDialogFragment.java index 171a256b..75f93bb2 100644 --- a/src/com/android/tv/dialog/WebDialogFragment.java +++ b/src/com/android/tv/dialog/WebDialogFragment.java @@ -37,7 +37,6 @@ public class WebDialogFragment extends SafeDismissDialogFragment { private static final String TITLE = "TITLE"; private static final String TRACKER_LABEL = "TRACKER_LABEL"; - private WebView mWebView; private String mTrackerLabel; /** @@ -74,21 +73,13 @@ public class WebDialogFragment extends SafeDismissDialogFragment { String title = getArguments().getString(TITLE); getDialog().setTitle(title); - mWebView = new WebView(getActivity()); - mWebView.setWebViewClient(new WebViewClient()); + WebView webView = new WebView(getActivity()); + webView.setWebViewClient(new WebViewClient()); String url = getArguments().getString(URL); - mWebView.loadUrl(url); + webView.loadUrl(url); Log.d(TAG, "Loading web content from " + url); - return mWebView; - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - if (mWebView != null) { - mWebView.destroy(); - } + return webView; } @Override diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java index a8637449..89661df3 100644 --- a/src/com/android/tv/dvr/BaseDvrDataManager.java +++ b/src/com/android/tv/dvr/BaseDvrDataManager.java @@ -26,10 +26,7 @@ import android.util.Log; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.dvr.data.RecordedProgram; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.ScheduledRecording.RecordingState; -import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ScheduledRecording.RecordingState; import com.android.tv.util.Clock; import java.util.ArrayList; @@ -321,41 +318,5 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } @Override - public void checkAndRemoveEmptySeriesRecording(long... seriesRecordingIds) { - List<SeriesRecording> toRemove = new ArrayList<>(); - for (long rId : seriesRecordingIds) { - SeriesRecording seriesRecording = getSeriesRecording(rId); - if (seriesRecording != null && isEmptySeriesRecording(seriesRecording)) { - toRemove.add(seriesRecording); - } - } - removeSeriesRecording(SeriesRecording.toArray(toRemove)); - } - - /** - * Returns {@code true}, if the series recording is empty and can be removed. If a series - * recording is in NORMAL state or has recordings or schedules, it is not empty and cannot be - * removed. - */ - protected final boolean isEmptySeriesRecording(@NonNull SeriesRecording seriesRecording) { - if (!seriesRecording.isStopped()) { - return false; - } - long seriesRecordingId = seriesRecording.getId(); - for (ScheduledRecording r : getAvailableScheduledRecordings()) { - if (r.getSeriesRecordingId() == seriesRecordingId) { - return false; - } - } - String seriesId = seriesRecording.getSeriesId(); - for (RecordedProgram r : getRecordedPrograms()) { - if (seriesId.equals(r.getSeriesId())) { - return false; - } - } - return true; - } - - @Override public void forgetStorage(String inputId) { } } diff --git a/src/com/android/tv/dvr/recorder/ConflictChecker.java b/src/com/android/tv/dvr/ConflictChecker.java index 8aa90116..201e379e 100644 --- a/src/com/android/tv/dvr/recorder/ConflictChecker.java +++ b/src/com/android/tv/dvr/ConflictChecker.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr.recorder; +package com.android.tv.dvr; import android.annotation.TargetApi; import android.content.ContentUris; @@ -37,9 +37,6 @@ import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.ui.DvrUiHelper; import java.util.ArrayList; import java.util.HashMap; diff --git a/src/com/android/tv/dvr/DvrDataManager.java b/src/com/android/tv/dvr/DvrDataManager.java index 6d400b82..06613667 100644 --- a/src/com/android/tv/dvr/DvrDataManager.java +++ b/src/com/android/tv/dvr/DvrDataManager.java @@ -21,10 +21,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Range; -import com.android.tv.dvr.data.RecordedProgram; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.ScheduledRecording.RecordingState; -import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ScheduledRecording.RecordingState; import java.util.Collection; import java.util.List; @@ -214,13 +211,6 @@ public interface DvrDataManager { Collection<Long> getDisallowedProgramIds(); /** - * Checks each of the give series recordings to see if it's empty, i.e., it doesn't contains - * any available schedules or recorded programs, and it's status is - * {@link SeriesRecording#STATE_SERIES_STOPPED}; and removes those empty series recordings. - */ - void checkAndRemoveEmptySeriesRecording(long... seriesRecordingIds); - - /** * Listens for the DVR schedules loading finished. */ interface OnDvrScheduleLoadFinishedListener { diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java index 6d0a9959..46682a48 100644 --- a/src/com/android/tv/dvr/DvrDataManagerImpl.java +++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java @@ -42,11 +42,7 @@ import android.util.Range; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.DvrStorageStatusManager.OnStorageMountChangedListener; -import com.android.tv.dvr.data.IdGenerator; -import com.android.tv.dvr.data.RecordedProgram; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.ScheduledRecording.RecordingState; -import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ScheduledRecording.RecordingState; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddScheduleTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddSeriesRecordingTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDeleteScheduleTask; @@ -55,8 +51,6 @@ import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryScheduleTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQuerySeriesRecordingTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateScheduleTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateSeriesRecordingTask; -import com.android.tv.dvr.provider.DvrDbSync; -import com.android.tv.dvr.recorder.SeriesRecordingScheduler; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.AsyncDbTask.AsyncRecordedProgramQueryTask; import com.android.tv.util.Clock; @@ -273,14 +267,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { removeScheduledRecording(ScheduledRecording.toArray(toDelete)); } IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId); - if (mRecordedProgramLoadFinished) { - validateSeriesRecordings(); - } mDvrLoadFinished = true; notifyDvrScheduleLoadFinished(); + mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); + mDbSync.start(); if (isInitialized()) { - mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); - mDbSync.start(); SeriesRecordingScheduler.getInstance(mContext).start(); } } @@ -315,9 +306,6 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (uri == null) { uri = RecordedPrograms.CONTENT_URI; } - if (recordedPrograms == null) { - recordedPrograms = Collections.emptyList(); - } int match = TvProviderUriMatcher.match(uri); if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM) { if (!mRecordedProgramLoadFinished) { @@ -330,11 +318,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } mRecordedProgramLoadFinished = true; notifyRecordedProgramLoadFinished(); - if (isInitialized()) { - mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); - mDbSync.start(); - } - } else if (recordedPrograms.isEmpty()) { + } else if (recordedPrograms == null || recordedPrograms.isEmpty()) { List<RecordedProgram> oldRecordedPrograms = new ArrayList<>(mRecordedPrograms.values()); mRecordedPrograms.clear(); @@ -371,7 +355,6 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } if (isInitialized()) { - validateSeriesRecordings(); SeriesRecordingScheduler.getInstance(mContext).start(); } } else if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM_ID) { @@ -380,15 +363,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } long id = ContentUris.parseId(uri); if (DEBUG) Log.d(TAG, "changed recorded program #" + id + " to " + recordedPrograms); - if (recordedPrograms.isEmpty()) { + if (recordedPrograms == null || recordedPrograms.isEmpty()) { mRecordedProgramsForRemovedInput.remove(id); RecordedProgram old = mRecordedPrograms.remove(id); if (old != null) { notifyRecordedProgramsRemoved(old); - SeriesRecording r = mSeriesId2SeriesRecordings.get(old.getSeriesId()); - if (r != null && isEmptySeriesRecording(r)) { - removeSeriesRecording(r); - } } } else { RecordedProgram recordedProgram = recordedPrograms.get(0); @@ -613,16 +592,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { public void removeScheduledRecording(boolean forceRemove, ScheduledRecording... schedules) { List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); List<ScheduledRecording> schedulesNotToDelete = new ArrayList<>(); - Set<Long> seriesRecordingIdsToCheck = new HashSet<>(); for (ScheduledRecording r : schedules) { mScheduledRecordings.remove(r.getId()); - getDeletedScheduleMap().remove(r.getProgramId()); + getDeletedScheduleMap().remove(r.getId()); mProgramId2ScheduledRecordings.remove(r.getProgramId()); - if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET - && (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED - || r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { - seriesRecordingIdsToCheck.add(r.getSeriesRecordingId()); - } boolean isScheduleForRemovedInput = mScheduledRecordingsForRemovedInput.remove(r.getProgramId()) != null; // If it belongs to the series recording and it's not started yet, just mark delete @@ -641,19 +614,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } if (mDvrLoadFinished) { - if (mRecordedProgramLoadFinished) { - checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck); - } notifyScheduledRecordingRemoved(schedules); } - Iterator<ScheduledRecording> iterator = schedulesNotToDelete.iterator(); - while (iterator.hasNext()) { - ScheduledRecording r = iterator.next(); - if (!mSeriesRecordings.containsKey(r.getSeriesRecordingId())) { - iterator.remove(); - schedulesToDelete.add(r); - } - } if (!schedulesToDelete.isEmpty()) { new AsyncDeleteScheduleTask(mContext).executeOnDbThread( ScheduledRecording.toArray(schedulesToDelete)); @@ -707,7 +669,6 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { private void updateScheduledRecording(boolean updateDb, final ScheduledRecording... schedules) { List<ScheduledRecording> toUpdate = new ArrayList<>(); - Set<Long> seriesRecordingIdsToCheck = new HashSet<>(); for (ScheduledRecording r : schedules) { if (!SoftPreconditions.checkState(mScheduledRecordings.containsKey(r.getId()), TAG, "Recording not found for: " + r)) { @@ -730,13 +691,6 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (programId != ScheduledRecording.ID_NOT_SET) { mProgramId2ScheduledRecordings.put(programId, r); } - if (r.getState() == ScheduledRecording.STATE_RECORDING_FAILED - && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) { - // If the scheduled recording is failed, it may cause the automatically generated - // series recording for this schedule becomes invalid (with no future schedules and - // past recordings.) We should check and remove these series recordings. - seriesRecordingIdsToCheck.add(r.getSeriesRecordingId()); - } } if (toUpdate.isEmpty()) { return; @@ -748,17 +702,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (updateDb) { new AsyncUpdateScheduleTask(mContext).executeOnDbThread(scheduleArray); } - checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck); removeDeletedSchedules(schedules); } @Override public void updateSeriesRecording(final SeriesRecording... seriesRecordings) { for (SeriesRecording r : seriesRecordings) { - if (!SoftPreconditions.checkArgument(mSeriesRecordings.containsKey(r.getId()), TAG, - "Non Existing Series ID: " + r)) { - continue; - } SeriesRecording old1 = mSeriesRecordings.put(r.getId(), r); SeriesRecording old2 = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); SoftPreconditions.checkArgument(old1.equals(old2), TAG, "Series ID cannot be" @@ -820,6 +769,14 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { return r.getInputId().equals(inputId); } }); + List<SeriesRecording> movedSeriesRecordings = + moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings, + new Filter<SeriesRecording>() { + @Override + public boolean filter(SeriesRecording r) { + return r.getInputId().equals(inputId); + } + }); List<RecordedProgram> movedRecordedPrograms = moveElements(mRecordedProgramsForRemovedInput, mRecordedPrograms, new Filter<RecordedProgram>() { @@ -828,21 +785,6 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { return r.getInputId().equals(inputId); } }); - List<SeriesRecording> removedSeriesRecordings = new ArrayList<>(); - List<SeriesRecording> movedSeriesRecordings = - moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings, - new Filter<SeriesRecording>() { - @Override - public boolean filter(SeriesRecording r) { - if (r.getInputId().equals(inputId)) { - if (!isEmptySeriesRecording(r)) { - return true; - } - removedSeriesRecordings.add(r); - } - return false; - } - }); if (!movedSchedules.isEmpty()) { for (ScheduledRecording schedule : movedSchedules) { mProgramId2ScheduledRecordings.put(schedule.getProgramId(), schedule); @@ -853,11 +795,6 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { mSeriesId2SeriesRecordings.put(seriesRecording.getSeriesId(), seriesRecording); } } - for (SeriesRecording r : removedSeriesRecordings) { - mSeriesRecordingsForRemovedInput.remove(r.getId()); - } - new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread( - SeriesRecording.toArray(removedSeriesRecordings)); // Notify after all the data are moved. if (!movedSchedules.isEmpty()) { notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules)); @@ -874,20 +811,20 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (DEBUG) Log.d(TAG, "hideInput " + inputId); List<ScheduledRecording> movedSchedules = moveElements(mScheduledRecordings, mScheduledRecordingsForRemovedInput, - new Filter<ScheduledRecording>() { - @Override - public boolean filter(ScheduledRecording r) { - return r.getInputId().equals(inputId); - } - }); + new Filter<ScheduledRecording>() { + @Override + public boolean filter(ScheduledRecording r) { + return r.getInputId().equals(inputId); + } + }); List<SeriesRecording> movedSeriesRecordings = moveElements(mSeriesRecordings, mSeriesRecordingsForRemovedInput, - new Filter<SeriesRecording>() { - @Override - public boolean filter(SeriesRecording r) { - return r.getInputId().equals(inputId); - } - }); + new Filter<SeriesRecording>() { + @Override + public boolean filter(SeriesRecording r) { + return r.getInputId().equals(inputId); + } + }); List<RecordedProgram> movedRecordedPrograms = moveElements(mRecordedPrograms, mRecordedProgramsForRemovedInput, new Filter<RecordedProgram>() { @@ -918,15 +855,6 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } - private void checkAndRemoveEmptySeriesRecording(Set<Long> seriesRecordingIds) { - int i = 0; - long[] rIds = new long[seriesRecordingIds.size()]; - for (long rId : seriesRecordingIds) { - rIds[i++] = rId; - } - checkAndRemoveEmptySeriesRecording(rIds); - } - @Override public void forgetStorage(String inputId) { List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); @@ -973,25 +901,6 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { }.executeOnDbThread(); } - private void validateSeriesRecordings() { - Iterator<SeriesRecording> iter = mSeriesRecordings.values().iterator(); - List<SeriesRecording> removedSeriesRecordings = new ArrayList<>(); - while (iter.hasNext()) { - SeriesRecording r = iter.next(); - if (isEmptySeriesRecording(r)) { - iter.remove(); - removedSeriesRecordings.add(r); - } - } - if (!removedSeriesRecordings.isEmpty()) { - SeriesRecording[] removed = SeriesRecording.toArray(removedSeriesRecordings); - new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(removed); - if (mDvrLoadFinished) { - notifySeriesRecordingRemoved(removed); - } - } - } - private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask { private final Uri mUri; diff --git a/src/com/android/tv/dvr/provider/DvrDbSync.java b/src/com/android/tv/dvr/DvrDbSync.java index 8a0c2d19..df181455 100644 --- a/src/com/android/tv/dvr/provider/DvrDbSync.java +++ b/src/com/android/tv/dvr/DvrDbSync.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr.provider; +package com.android.tv.dvr; import android.annotation.SuppressLint; import android.annotation.TargetApi; @@ -34,11 +34,6 @@ import com.android.tv.TvApplication; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.dvr.DvrDataManagerImpl; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.SeriesRecording; -import com.android.tv.dvr.recorder.SeriesRecordingScheduler; import com.android.tv.util.AsyncDbTask.AsyncQueryProgramTask; import com.android.tv.util.TvProviderUriMatcher; @@ -62,12 +57,11 @@ import java.util.Set; */ @MainThread @TargetApi(Build.VERSION_CODES.N) -public class DvrDbSync { +class DvrDbSync { private static final String TAG = "DvrDbSync"; private static final boolean DEBUG = false; private final Context mContext; - private final DvrManager mDvrManager; private final DvrDataManagerImpl mDataManager; private final ChannelDataManager mChannelDataManager; private final Queue<Long> mProgramIdQueue = new LinkedList<>(); @@ -135,21 +129,17 @@ public class DvrDbSync { } }; - public DvrDbSync(Context context, DvrDataManagerImpl dataManager) { - this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager(), - TvApplication.getSingletons(context).getDvrManager(), - SeriesRecordingScheduler.getInstance(context)); + DvrDbSync(Context context, DvrDataManagerImpl dataManager) { + this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager()); } @VisibleForTesting DvrDbSync(Context context, DvrDataManagerImpl dataManager, - ChannelDataManager channelDataManager, DvrManager dvrManager, - SeriesRecordingScheduler seriesRecordingScheduler) { + ChannelDataManager channelDataManager) { mContext = context; - mDvrManager = dvrManager; mDataManager = dataManager; mChannelDataManager = channelDataManager; - mSeriesRecordingScheduler = seriesRecordingScheduler; + mSeriesRecordingScheduler = SeriesRecordingScheduler.getInstance(context); } /** @@ -289,9 +279,10 @@ public class DvrDbSync { mDataManager.getSeriesRecording(program.getSeriesId()); if (seriesRecording == null) { // The new program is episodic while the previous one isn't. - SeriesRecording newSeriesRecording = mDvrManager.addSeriesRecording( - program, Collections.singletonList(program), - SeriesRecording.STATE_SERIES_STOPPED); + SeriesRecording newSeriesRecording = TvApplication.getSingletons(mContext) + .getDvrManager().addSeriesRecording(program, + Collections.singletonList(program), + SeriesRecording.STATE_SERIES_STOPPED); builder.setSeriesRecordingId(newSeriesRecording.getId()); needUpdate = true; } else if (seriesRecording.getId() != schedule.getSeriesRecordingId()) { @@ -315,9 +306,8 @@ public class DvrDbSync { // Old program belongs to a series but the new one doesn't. seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); } - // Change start time only when the recording is not started yet. - boolean needToChangeStartTime = - schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS + // Change start time only when the recording start time has not passed. + boolean needToChangeStartTime = schedule.getStartTimeMs() > currentTimeMs && program.getStartTimeUtcMillis() != schedule.getStartTimeMs(); if (needToChangeStartTime) { builder.setStartTimeMs(program.getStartTimeUtcMillis()); diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java index 2effe9cf..5fa6f90f 100644 --- a/src/com/android/tv/dvr/DvrManager.java +++ b/src/com/android/tv/dvr/DvrManager.java @@ -46,9 +46,7 @@ import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; import com.android.tv.dvr.DvrScheduleManager.OnInitializeListener; -import com.android.tv.dvr.data.RecordedProgram; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.SeriesRecording.SeriesState; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.Utils; @@ -236,7 +234,7 @@ public class DvrManager { * Adds a new series recording and schedules for the programs with the initial state. */ public SeriesRecording addSeriesRecording(Program selectedProgram, - List<Program> programsToSchedule, @SeriesRecording.SeriesState int initialState) { + List<Program> programsToSchedule, @SeriesState int initialState) { Log.i(TAG, "Adding series recording for program " + selectedProgram + ", and schedules: " + programsToSchedule); if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { @@ -310,7 +308,8 @@ public class DvrManager { ScheduledRecording scheduleWithSameProgram = mDataManager.getScheduledRecordingForProgramId(program.getId()); if (scheduleWithSameProgram != null) { - if (scheduleWithSameProgram.isNotStarted()) { + if (scheduleWithSameProgram.getState() + == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { ScheduledRecording r = ScheduledRecording.buildFrom(scheduleWithSameProgram) .setSeriesRecordingId(series.getId()) .build(); @@ -338,10 +337,10 @@ public class DvrManager { */ public void updateSeriesRecording(SeriesRecording series) { if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { + SeriesRecordingScheduler scheduler = SeriesRecordingScheduler.getInstance(mAppContext); + scheduler.pauseUpdate(); SeriesRecording previousSeries = mDataManager.getSeriesRecording(series.getId()); if (previousSeries != null) { - // If the channel option of series changed, remove the existing schedules. The new - // schedules will be added by SeriesRecordingScheduler or by SeriesSettingsFragment. if (previousSeries.getChannelOption() != series.getChannelOption() || (previousSeries.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE && previousSeries.getChannelId() != series.getChannelId())) { @@ -351,18 +350,6 @@ public class DvrManager { for (ScheduledRecording schedule : schedules) { if (schedule.isNotStarted()) { schedulesToRemove.add(schedule); - } else if (schedule.isInProgress() - && series.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE - && schedule.getChannelId() != series.getChannelId()) { - stopRecording(schedule); - } - } - List<ScheduledRecording> deletedSchedules = - new ArrayList<>(mDataManager.getDeletedSchedules()); - for (ScheduledRecording deletedSchedule : deletedSchedules) { - if (deletedSchedule.getSeriesRecordingId() == series.getId() - && deletedSchedule.getEndTimeMs() > System.currentTimeMillis()) { - schedulesToRemove.add(deletedSchedule); } } mDataManager.removeScheduledRecording(true, @@ -376,7 +363,7 @@ public class DvrManager { List<ScheduledRecording> schedulesToUpdate = new ArrayList<>(); for (ScheduledRecording schedule : mDataManager.getScheduledRecordings(series.getId())) { - if (schedule.isNotStarted() || schedule.isInProgress()) { + if (schedule.isNotStarted()) { schedulesToUpdate.add(ScheduledRecording.buildFrom(schedule) .setPriority(priority).build()); } @@ -386,6 +373,7 @@ public class DvrManager { ScheduledRecording.toArray(schedulesToUpdate)); } } + scheduler.resumeUpdate(); } } @@ -412,6 +400,33 @@ public class DvrManager { } /** + * Returns true, if the series recording can be removed. If a series recording is NORMAL state + * or has recordings or schedules, it cannot be removed. + */ + public boolean canRemoveSeriesRecording(long seriesRecordingId) { + SeriesRecording seriesRecording = mDataManager.getSeriesRecording(seriesRecordingId); + if (seriesRecording == null) { + return false; + } + if (!seriesRecording.isStopped()) { + return false; + } + for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) { + if (r.getSeriesRecordingId() == seriesRecordingId) { + return false; + } + } + String seriesId = seriesRecording.getSeriesId(); + SoftPreconditions.checkNotNull(seriesId); + for (RecordedProgram r : mDataManager.getRecordedPrograms()) { + if (seriesId.equals(r.getSeriesId())) { + return false; + } + } + return true; + } + + /** * Stops the currently recorded program */ public void stopRecording(final ScheduledRecording recording) { @@ -642,9 +657,6 @@ public class DvrManager { if (!mDataManager.isDvrScheduleLoadFinished() || channel == null) { return false; } - if (channel.isRecordingProhibited()) { - return false; - } TvInputInfo info = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId()); if (info == null) { Log.w(TAG, "Could not find TvInputInfo for " + channel); @@ -668,12 +680,7 @@ public class DvrManager { if (!mDataManager.isInitialized()) { return false; } - Channel channel = TvApplication.getSingletons(mAppContext).getChannelDataManager() - .getChannel(program.getChannelId()); - if (channel == null || channel.isRecordingProhibited()) { - return false; - } - TvInputInfo info = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId()); + TvInputInfo info = Utils.getTvInputInfoForProgram(mAppContext, program); if (info == null) { Log.w(TAG, "Could not find TvInputInfo for " + program); return false; @@ -726,17 +733,6 @@ public class DvrManager { return mDataManager.getSeriesRecording(program.getSeriesId()); } - /** - * Returns if there are valid items. Valid item contains {@link RecordedProgram}, - * available {@link ScheduledRecording} and {@link SeriesRecording}. - */ - public boolean hasValidItems() { - return !(mDataManager.getRecordedPrograms().isEmpty() - && mDataManager.getStartedRecordings().isEmpty() - && mDataManager.getNonStartedScheduledRecordings().isEmpty() - && mDataManager.getSeriesRecordings().isEmpty()); - } - @WorkerThread @VisibleForTesting // Should be public to use mock DvrManager object. @@ -844,10 +840,9 @@ public class DvrManager { } /** - * Listener to stop recording request. Should only be internally used inside dvr and its - * sub-package. + * Listener internally used inside dvr package. */ - public interface Listener { + interface Listener { void onStopRecordingRequested(ScheduledRecording scheduledRecording); } } diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java b/src/com/android/tv/dvr/DvrPlaybackActivity.java index 2437d1f5..5deda44a 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java +++ b/src/com/android/tv/dvr/DvrPlaybackActivity.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui.playback; +package com.android.tv.dvr; import android.app.Activity; import android.content.Intent; @@ -24,7 +24,7 @@ import android.util.Log; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment; /** * Activity to play a {@link RecordedProgram}. diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java index 843d2dbe..9759a856 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java +++ b/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui.playback; +package com.android.tv.dvr; import android.app.Activity; import android.content.Intent; @@ -31,16 +31,14 @@ import android.text.TextUtils; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment; import com.android.tv.util.ImageLoader; import com.android.tv.util.TimeShiftUtils; import com.android.tv.util.Utils; -class DvrPlaybackMediaSessionHelper { +public class DvrPlaybackMediaSessionHelper { private static final String TAG = "DvrPlaybackMediaSessionHelper"; private static final boolean DEBUG = false; @@ -104,7 +102,6 @@ class DvrPlaybackMediaSessionHelper { } if (mMediaSession != null) { mMediaSession.release(); - mMediaSession = null; } } @@ -182,88 +179,83 @@ class DvrPlaybackMediaSessionHelper { cardTitleText = (channel != null) ? channel.getDisplayName() : mActivity.getString(R.string.no_program_information); } - final MediaMetadata currentMetadata = updateMetadataTextInfo(program.getId(), cardTitleText, - program.getDescription(), mProgramDurationMs); + updateMediaMetadata(program.getId(), cardTitleText, program.getDescription(), + mProgramDurationMs, null, 0); String posterArtUri = program.getPosterArtUri(); if (posterArtUri == null) { posterArtUri = TvContract.buildChannelLogoUri(program.getChannelId()).toString(); } - updatePosterArt(program, currentMetadata, null, posterArtUri); + updatePosterArt(program, cardTitleText, program.getDescription(), + mProgramDurationMs, null, posterArtUri); mMediaSession.setActive(true); } - private void updatePosterArt(RecordedProgram program, MediaMetadata currentMetadata, + private void updatePosterArt(RecordedProgram program, String cardTitleText, + String cardSubtitleText, long duration, @Nullable Bitmap posterArt, @Nullable String posterArtUri) { if (posterArt != null) { - updateMetadataImageInfo(program, currentMetadata, posterArt, 0); + updateMediaMetadata(program.getId(), cardTitleText, + cardSubtitleText, duration, posterArt, 0); } else if (posterArtUri != null) { ImageLoader.loadBitmap(mActivity, posterArtUri, mNowPlayingCardWidth, - mNowPlayingCardHeight, - new ProgramPosterArtCallback(mActivity, program, currentMetadata)); + mNowPlayingCardHeight, new ProgramPosterArtCallback( + mActivity, program, cardTitleText, cardSubtitleText, duration)); } else { - updateMetadataImageInfo(program, currentMetadata, null, R.drawable.default_now_card); + updateMediaMetadata(program.getId(), cardTitleText, + cardSubtitleText, duration, null, R.drawable.default_now_card); } } private class ProgramPosterArtCallback extends ImageLoader.ImageLoaderCallback<Activity> { - private final RecordedProgram mRecordedProgram; - private final MediaMetadata mCurrentMetadata; + private RecordedProgram mRecordedProgram; + private String mCardTitleText; + private String mCardSubtitleText; + private long mDuration; public ProgramPosterArtCallback(Activity activity, RecordedProgram program, - MediaMetadata metadata) { + String cardTitleText, String cardSubtitleText, long duration) { super(activity); mRecordedProgram = program; - mCurrentMetadata = metadata; + mCardTitleText = cardTitleText; + mCardSubtitleText = cardSubtitleText; + mDuration = duration; } @Override public void onBitmapLoaded(Activity activity, @Nullable Bitmap posterArt) { if (isCurrentProgram(mRecordedProgram)) { - updatePosterArt(mRecordedProgram, mCurrentMetadata, posterArt, null); + updatePosterArt(mRecordedProgram, mCardTitleText, + mCardSubtitleText, mDuration, posterArt, null); } } } - private MediaMetadata updateMetadataTextInfo(final long programId, final String title, - final String subtitle, final long duration) { - MediaMetadata.Builder builder = new MediaMetadata.Builder(); - builder.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Long.toString(programId)) - .putString(MediaMetadata.METADATA_KEY_TITLE, title) - .putLong(MediaMetadata.METADATA_KEY_DURATION, duration); - if (subtitle != null) { - builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle); - } - MediaMetadata metadata = builder.build(); - mMediaSession.setMetadata(metadata); - return metadata; - } - - private void updateMetadataImageInfo(final RecordedProgram program, - final MediaMetadata currentMetadata, final Bitmap posterArt, final int imageResId) { - if (mMediaSession != null && (posterArt != null || imageResId != 0)) { - MediaMetadata.Builder builder = new MediaMetadata.Builder(currentMetadata); - if (posterArt != null) { - builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt); + private void updateMediaMetadata(final long programId, final String title, + final String subtitle, final long duration, + final Bitmap posterArt, final int imageResId) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... arg0) { + MediaMetadata.Builder builder = new MediaMetadata.Builder(); + builder.putLong(MediaMetadata.METADATA_KEY_MEDIA_ID, programId) + .putString(MediaMetadata.METADATA_KEY_TITLE, title) + .putLong(MediaMetadata.METADATA_KEY_DURATION, duration); + if (subtitle != null) { + builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle); + } + Bitmap programPosterArt = posterArt; + if (programPosterArt == null && imageResId != 0) { + programPosterArt = + BitmapFactory.decodeResource(mActivity.getResources(), imageResId); + } + if (programPosterArt != null) { + builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt); + } mMediaSession.setMetadata(builder.build()); - } else { - new AsyncTask<Void, Void, Bitmap>() { - @Override - protected Bitmap doInBackground(Void... arg0) { - return BitmapFactory.decodeResource(mActivity.getResources(), imageResId); - } - - @Override - protected void onPostExecute(Bitmap programPosterArt) { - if (mMediaSession != null && programPosterArt != null - && isCurrentProgram(program)) { - builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt); - mMediaSession.setMetadata(builder.build()); - } - } - }.execute(); + return null; } - } + }.execute(); } // An event was triggered by MediaController.TransportControls and must be handled here. diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java b/src/com/android/tv/dvr/DvrPlayer.java index 780bfb2f..5656655c 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java +++ b/src/com/android/tv/dvr/DvrPlayer.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui.playback; +package com.android.tv.dvr; import android.media.PlaybackParams; import android.media.tv.TvContentRating; @@ -22,16 +22,12 @@ import android.media.tv.TvInputManager; import android.media.tv.TvTrackInfo; import android.media.tv.TvView; import android.media.session.PlaybackState; -import android.text.TextUtils; import android.util.Log; -import com.android.tv.dvr.data.RecordedProgram; - -import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; -class DvrPlayer { +public class DvrPlayer { private static final String TAG = "DvrPlayer"; private static final boolean DEBUG = false; @@ -51,19 +47,12 @@ class DvrPlayer { private long mInitialSeekPositionMs; private final TvView mTvView; private DvrPlayerCallback mCallback; - private OnAspectRatioChangedListener mOnAspectRatioChangedListener; - private OnContentBlockedListener mOnContentBlockedListener; - private OnTracksAvailabilityChangedListener mOnTracksAvailabilityChangedListener; - private OnTrackSelectedListener mOnAudioTrackSelectedListener; - private OnTrackSelectedListener mOnSubtitleTrackSelectedListener; - private String mSelectedAudioTrackId; - private String mSelectedSubtitleTrackId; + private AspectRatioChangedListener mAspectRatioChangedListener; + private ContentBlockedListener mContentBlockedListener; private float mAspectRatio = Float.NaN; private int mPlaybackState = PlaybackState.STATE_NONE; private long mTimeShiftCurrentPositionMs; private boolean mPauseOnPrepared; - private boolean mHasClosedCaption; - private boolean mHasMultiAudio; private final PlaybackParams mPlaybackParams = new PlaybackParams(); private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback(); private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; @@ -86,40 +75,22 @@ class DvrPlayer { public void onPlaybackEnded() { } } - public interface OnAspectRatioChangedListener { + public interface AspectRatioChangedListener { /** * Called when the Video's aspect ratio is changed. - * - * @param videoAspectRatio The aspect ratio of video. 0 stands for unknown ratios. - * Listeners should handle it carefully. */ void onAspectRatioChanged(float videoAspectRatio); } - public interface OnContentBlockedListener { + public interface ContentBlockedListener { /** * Called when the Video's aspect ratio is changed. */ void onContentBlocked(TvContentRating rating); } - public interface OnTracksAvailabilityChangedListener { - /** - * Called when the Video's subtitle or audio tracks are changed. - */ - void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio); - } - - public interface OnTrackSelectedListener { - /** - * Called when certain subtitle or audio track is selected. - */ - void onTrackSelected(String selectedTrackId); - } - public DvrPlayer(TvView tvView) { mTvView = tvView; - mTvView.setCaptionEnabled(true); mPlaybackParams.setSpeed(1.0f); setTvViewCallbacks(); setCallback(null); @@ -265,8 +236,6 @@ class DvrPlayer { mTimeShiftCurrentPositionMs = 0; mPlaybackParams.setSpeed(1.0f); mProgram = null; - mSelectedAudioTrackId = null; - mSelectedSubtitleTrackId = null; } /** @@ -281,51 +250,17 @@ class DvrPlayer { } /** - * Sets the listener to aspect ratio changing. - */ - public void setOnAspectRatioChangedListener(OnAspectRatioChangedListener listener) { - mOnAspectRatioChangedListener = listener; - } - - /** - * Sets the listener to content blocking. - */ - public void setOnContentBlockedListener(OnContentBlockedListener listener) { - mOnContentBlockedListener = listener; - } - - /** - * Sets the listener to tracks changing. - */ - public void setOnTracksAvailabilityChangedListener( - OnTracksAvailabilityChangedListener listener) { - mOnTracksAvailabilityChangedListener = listener; - } - - /** - * Sets the listener to tracks of the given type being selected. - * - * @param trackType should be either {@link TvTrackInfo#TYPE_AUDIO} - * or {@link TvTrackInfo#TYPE_SUBTITLE}. + * Sets listener to aspect ratio changing. */ - public void setOnTrackSelectedListener(int trackType, OnTrackSelectedListener listener) { - if (trackType == TvTrackInfo.TYPE_AUDIO) { - mOnAudioTrackSelectedListener = listener; - } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { - mOnSubtitleTrackSelectedListener = listener; - } + public void setAspectRatioChangedListener(AspectRatioChangedListener listener) { + mAspectRatioChangedListener = listener; } /** - * Gets the listener to tracks of the given type being selected. + * Sets listener to content blocking. */ - public OnTrackSelectedListener getOnTrackSelectedListener(int trackType) { - if (trackType == TvTrackInfo.TYPE_AUDIO) { - return mOnAudioTrackSelectedListener; - } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { - return mOnSubtitleTrackSelectedListener; - } - return null; + public void setContentBlockedListener(ContentBlockedListener listener) { + mContentBlockedListener = listener; } /** @@ -371,32 +306,6 @@ class DvrPlayer { } /** - * Returns the subtitle tracks of the current playback. - */ - public ArrayList<TvTrackInfo> getSubtitleTracks() { - return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE)); - } - - /** - * Returns the audio tracks of the current playback. - */ - public ArrayList<TvTrackInfo> getAudioTracks() { - return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_AUDIO)); - } - - /** - * Returns the ID of the selected track of the given type. - */ - public String getSelectedTrackId(int trackType) { - if (trackType == TvTrackInfo.TYPE_AUDIO) { - return mSelectedAudioTrackId; - } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { - return mSelectedSubtitleTrackId; - } - return null; - } - - /** * Returns if playback of the recorded program is started. */ public boolean isPlaybackPrepared() { @@ -404,41 +313,6 @@ class DvrPlayer { && mPlaybackState != PlaybackState.STATE_CONNECTING; } - /** - * Selects the given track. - * - * @return ID of the selected track. - */ - String selectTrack(int trackType, TvTrackInfo selectedTrack) { - String oldSelectedTrackId = getSelectedTrackId(trackType); - String newSelectedTrackId = selectedTrack == null ? null : selectedTrack.getId(); - if (!TextUtils.equals(oldSelectedTrackId, newSelectedTrackId)) { - if (selectedTrack == null) { - mTvView.selectTrack(trackType, null); - return null; - } else { - List<TvTrackInfo> tracks = mTvView.getTracks(trackType); - if (tracks != null && tracks.contains(selectedTrack)) { - mTvView.selectTrack(trackType, newSelectedTrackId); - return newSelectedTrackId; - } else if (trackType == TvTrackInfo.TYPE_SUBTITLE && oldSelectedTrackId != null) { - // Track not found, disabled closed caption. - mTvView.selectTrack(trackType, null); - return null; - } - } - } - return oldSelectedTrackId; - } - - private void setSelectedTrackId(int trackType, String trackId) { - if (trackType == TvTrackInfo.TYPE_AUDIO) { - mSelectedAudioTrackId = trackId; - } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { - mSelectedSubtitleTrackId = trackId; - } - } - private void setPlaybackSpeed(int speed) { mPlaybackParams.setSpeed(speed); mTvView.timeShiftSetPlaybackParams(mPlaybackParams); @@ -495,60 +369,28 @@ class DvrPlayer { if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE && mPlaybackState == PlaybackState.STATE_CONNECTING) { mTimeShiftPlayAvailable = true; - if (mStartPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { - // onTimeShiftStatusChanged is sometimes called after - // onTimeShiftStartPositionChanged is called. In this case, - // resumeToWatchedPositionIfNeeded needs to be called here. - resumeToWatchedPositionIfNeeded(); - } } } @Override - public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { - boolean hasClosedCaption = - !mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE).isEmpty(); - boolean hasMultiAudio = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO).size() > 1; - if ((hasClosedCaption != mHasClosedCaption || hasMultiAudio != mHasMultiAudio) - && mOnTracksAvailabilityChangedListener != null) { - mOnTracksAvailabilityChangedListener - .onTracksAvailabilityChanged(hasClosedCaption, hasMultiAudio); - } - mHasClosedCaption = hasClosedCaption; - mHasMultiAudio = hasMultiAudio; - } - - @Override public void onTrackSelected(String inputId, int type, String trackId) { - if (type == TvTrackInfo.TYPE_AUDIO || type == TvTrackInfo.TYPE_SUBTITLE) { - setSelectedTrackId(type, trackId); - OnTrackSelectedListener listener = getOnTrackSelectedListener(type); - if (listener != null) { - listener.onTrackSelected(trackId); - } - } else if (type == TvTrackInfo.TYPE_VIDEO && trackId != null - && mOnAspectRatioChangedListener != null) { - List<TvTrackInfo> trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO); - if (trackInfos != null) { - for (TvTrackInfo trackInfo : trackInfos) { - if (trackInfo.getId().equals(trackId)) { - float videoAspectRatio; - int videoWidth = trackInfo.getVideoWidth(); - int videoHeight = trackInfo.getVideoHeight(); - if (videoWidth > 0 && videoHeight > 0) { - videoAspectRatio = trackInfo.getVideoPixelAspectRatio() - * trackInfo.getVideoWidth() / trackInfo.getVideoHeight(); - } else { - // Aspect ratio is unknown. Pass the message to listeners. - videoAspectRatio = 0; - } - if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio); - if (mAspectRatio != videoAspectRatio || videoAspectRatio == 0) { - mOnAspectRatioChangedListener - .onAspectRatioChanged(videoAspectRatio); - mAspectRatio = videoAspectRatio; - return; - } + if (trackId == null || type != TvTrackInfo.TYPE_VIDEO + || mAspectRatioChangedListener == null) { + return; + } + List<TvTrackInfo> trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO); + if (trackInfos != null) { + for (TvTrackInfo trackInfo : trackInfos) { + if (trackInfo.getId().equals(trackId)) { + float videoAspectRatio = trackInfo.getVideoPixelAspectRatio() + * trackInfo.getVideoWidth() / trackInfo.getVideoHeight(); + if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio); + if (!Float.isNaN(videoAspectRatio) + && mAspectRatio != videoAspectRatio) { + mAspectRatioChangedListener + .onAspectRatioChanged(videoAspectRatio); + mAspectRatio = videoAspectRatio; + return; } } } @@ -557,8 +399,8 @@ class DvrPlayer { @Override public void onContentBlocked(String inputId, TvContentRating rating) { - if (mOnContentBlockedListener != null) { - mOnContentBlockedListener.onContentBlocked(rating); + if (mContentBlockedListener != null) { + mContentBlockedListener.onContentBlocked(rating); } } }); diff --git a/src/com/android/tv/dvr/recorder/DvrRecordingService.java b/src/com/android/tv/dvr/DvrRecordingService.java index 08ffaf86..8c40aaa8 100644 --- a/src/com/android/tv/dvr/recorder/DvrRecordingService.java +++ b/src/com/android/tv/dvr/DvrRecordingService.java @@ -14,10 +14,9 @@ * limitations under the License */ -package com.android.tv.dvr.recorder; +package com.android.tv.dvr; import android.app.AlarmManager; -import android.app.Notification; import android.app.Service; import android.content.Context; import android.content.Intent; @@ -28,13 +27,9 @@ import android.support.annotation.VisibleForTesting; import android.util.Log; import com.android.tv.ApplicationSingletons; -import com.android.tv.InputSessionManager; -import com.android.tv.InputSessionManager.OnRecordingSessionChangeListener; -import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.dvr.WritableDvrDataManager; import com.android.tv.util.Clock; import com.android.tv.util.RecurringRunner; @@ -57,8 +52,6 @@ public class DvrRecordingService extends Service { private static final boolean DEBUG = false; public static final String HANDLER_THREAD_NAME = "DvrRecordingService-handler"; - private static final int ONGOING_NOTIFICATION_ID = 1; - public static void startService(Context context) { Intent dvrSchedulerIntent = new Intent(context, DvrRecordingService.class); context.startService(dvrSchedulerIntent); @@ -69,27 +62,6 @@ public class DvrRecordingService extends Service { private Scheduler mScheduler; private HandlerThread mHandlerThread; - private InputSessionManager mSessionManager; - private boolean mForeground; - - private final OnRecordingSessionChangeListener mOnRecordingSessionChangeListener = - new OnRecordingSessionChangeListener() { - @Override - public void onRecordingSessionChange(final boolean create, final int count) { - if (create && !mForeground) { - Notification notification = - new Notification.Builder(getApplicationContext()) - .setContentTitle(TAG) - .setSmallIcon(R.drawable.ic_dvr) - .build(); - startForeground(ONGOING_NOTIFICATION_ID, notification); - mForeground = true; - } else if (!create && mForeground && count == 0) { - stopForeground(STOP_FOREGROUND_REMOVE); - mForeground = false; - } - } - }; @Override public void onCreate() { @@ -98,11 +70,8 @@ public class DvrRecordingService extends Service { super.onCreate(); SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG); ApplicationSingletons singletons = TvApplication.getSingletons(this); - WritableDvrDataManager dataManager = - (WritableDvrDataManager) singletons.getDvrDataManager(); - mSessionManager = singletons.getInputSessionManager(); + WritableDvrDataManager dataManager = (WritableDvrDataManager) singletons.getDvrDataManager(); - mSessionManager.addOnRecordingSessionChangeListener(mOnRecordingSessionChangeListener); AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); // mScheduler may have been set for testing. if (mScheduler == null) { @@ -136,7 +105,6 @@ public class DvrRecordingService extends Service { mHandlerThread.quitSafely(); mHandlerThread = null; } - mSessionManager.removeRecordingSessionChangeListener(mOnRecordingSessionChangeListener); super.onDestroy(); } diff --git a/src/com/android/tv/dvr/DvrScheduleManager.java b/src/com/android/tv/dvr/DvrScheduleManager.java index b72117aa..a5851a75 100644 --- a/src/com/android/tv/dvr/DvrScheduleManager.java +++ b/src/com/android/tv/dvr/DvrScheduleManager.java @@ -24,6 +24,7 @@ import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.util.ArraySet; +import android.util.LongSparseArray; import android.util.Range; import com.android.tv.ApplicationSingletons; @@ -34,10 +35,7 @@ import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.dvr.recorder.InputTaskScheduler; import com.android.tv.util.CompositeComparator; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.Utils; import java.util.ArrayList; @@ -90,8 +88,9 @@ public class DvrScheduleManager { private final Map<String, List<ScheduledRecording>> mInputScheduleMap = new HashMap<>(); // The inner map is a hash map from scheduled recording to its conflicting status, i.e., // the boolean value true denotes the schedule is just partially conflicting, which means - // although there's conflict, it might still be recorded partially. - private final Map<String, Map<Long, ConflictInfo>> mInputConflictInfoMap = new HashMap<>(); + // although there's conflictit, it might still be recorded partially. + private final Map<String, Map<ScheduledRecording, Boolean>> mInputConflictInfoMap = + new HashMap<>(); private boolean mInitialized; @@ -172,9 +171,10 @@ public class DvrScheduleManager { mInputScheduleMap.remove(inputId); } } - Map<Long, ConflictInfo> conflictInfo = mInputConflictInfoMap.get(inputId); + Map<ScheduledRecording, Boolean> conflictInfo = + mInputConflictInfoMap.get(inputId); if (conflictInfo != null) { - conflictInfo.remove(schedule.getId()); + conflictInfo.remove(schedule); if (conflictInfo.isEmpty()) { mInputConflictInfoMap.remove(inputId); } @@ -221,11 +221,21 @@ public class DvrScheduleManager { mInputScheduleMap.remove(inputId); } // Update conflict list as well - Map<Long, ConflictInfo> conflictInfo = mInputConflictInfoMap.get(inputId); + Map<ScheduledRecording, Boolean> conflictInfo = + mInputConflictInfoMap.get(inputId); if (conflictInfo != null) { - ConflictInfo oldConflictInfo = conflictInfo.get(schedule.getId()); - if (oldConflictInfo != null) { - oldConflictInfo.schedule = schedule; + // Compare ID because ScheduledRecording.equals() doesn't work if the state + // is changed. + ScheduledRecording oldSchedule = null; + for (ScheduledRecording s : conflictInfo.keySet()) { + if (s.getId() == schedule.getId()) { + oldSchedule = s; + break; + } + } + if (oldSchedule != null) { + conflictInfo.put(schedule, conflictInfo.get(oldSchedule)); + conflictInfo.remove(oldSchedule); } } } @@ -307,25 +317,24 @@ public class DvrScheduleManager { List<ScheduledRecording> addedConflicts = new ArrayList<>(); List<ScheduledRecording> removedConflicts = new ArrayList<>(); for (String inputId : mInputScheduleMap.keySet()) { - Map<Long, ConflictInfo> oldConflictInfo = mInputConflictInfoMap.get(inputId); + Map<ScheduledRecording, Boolean> oldConflictsInfo = mInputConflictInfoMap.get(inputId); Map<Long, ScheduledRecording> oldConflictMap = new HashMap<>(); - if (oldConflictInfo != null) { - for (ConflictInfo conflictInfo : oldConflictInfo.values()) { - oldConflictMap.put(conflictInfo.schedule.getId(), conflictInfo.schedule); + if (oldConflictsInfo != null) { + for (ScheduledRecording r : oldConflictsInfo.keySet()) { + oldConflictMap.put(r.getId(), r); } } - List<ConflictInfo> conflicts = getConflictingSchedulesInfo(inputId); - if (conflicts.isEmpty()) { + Map<ScheduledRecording, Boolean> conflictInfo = getConflictingSchedulesInfo(inputId); + if (conflictInfo.isEmpty()) { mInputConflictInfoMap.remove(inputId); } else { - Map<Long, ConflictInfo> conflictInfos = new HashMap<>(); - for (ConflictInfo conflictInfo : conflicts) { - conflictInfos.put(conflictInfo.schedule.getId(), conflictInfo); - if (oldConflictMap.remove(conflictInfo.schedule.getId()) == null) { - addedConflicts.add(conflictInfo.schedule); + mInputConflictInfoMap.put(inputId, conflictInfo); + List<ScheduledRecording> conflicts = new ArrayList<>(conflictInfo.keySet()); + for (ScheduledRecording r : conflicts) { + if (oldConflictMap.remove(r.getId()) == null) { + addedConflicts.add(r); } } - mInputConflictInfoMap.put(inputId, conflictInfos); } removedConflicts.addAll(oldConflictMap.values()); } @@ -556,7 +565,8 @@ public class DvrScheduleManager { } /** - * Returns list of all conflicting scheduled recordings for the given {@code seriesRecording} + * Returns list of all conflicting scheduled recordings with schedules belonging to {@code + * seriesRecording} * recording. * <p> * Any empty list means there is no conflicts. @@ -571,18 +581,9 @@ public class DvrScheduleManager { if (input == null || !input.canRecord() || input.getTunerCount() <= 0) { return Collections.emptyList(); } - List<ScheduledRecording> scheduledRecordingForSeries = mDataManager.getScheduledRecordings( + List<ScheduledRecording> schedulesForSeries = mDataManager.getScheduledRecordings( seriesRecording.getId()); - List<ScheduledRecording> availableScheduledRecordingForSeries = new ArrayList<>(); - for (ScheduledRecording scheduledRecording : scheduledRecordingForSeries) { - if (scheduledRecording.isNotStarted() || scheduledRecording.isInProgress()) { - availableScheduledRecordingForSeries.add(scheduledRecording); - } - } - if (availableScheduledRecordingForSeries.isEmpty()) { - return Collections.emptyList(); - } - return getConflictingSchedules(input, availableScheduledRecordingForSeries); + return getConflictingSchedules(input, schedulesForSeries); } /** @@ -616,16 +617,16 @@ public class DvrScheduleManager { * the given input. */ @NonNull - private List<ConflictInfo> getConflictingSchedulesInfo(String inputId) { + private Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo(String inputId) { SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, inputId); SoftPreconditions.checkState(input != null, TAG, "Can't find input for : " + inputId); if (!mInitialized || input == null) { - return Collections.emptyList(); + return Collections.emptyMap(); } List<ScheduledRecording> schedules = mInputScheduleMap.get(input.getId()); if (schedules == null || schedules.isEmpty()) { - return Collections.emptyList(); + return Collections.emptyMap(); } return getConflictingSchedulesInfo(schedules, input.getTunerCount()); } @@ -644,8 +645,8 @@ public class DvrScheduleManager { if (!mInitialized || input == null) { return false; } - Map<Long, ConflictInfo> conflicts = mInputConflictInfoMap.get(input.getId()); - return conflicts != null && conflicts.containsKey(schedule.getId()); + Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId()); + return conflicts != null && conflicts.containsKey(schedule); } /** @@ -663,12 +664,8 @@ public class DvrScheduleManager { if (!mInitialized || input == null) { return false; } - Map<Long, ConflictInfo> conflicts = mInputConflictInfoMap.get(input.getId()); - if (conflicts != null) { - ConflictInfo conflictInfo = conflicts.get(schedule.getId()); - return conflictInfo != null && conflictInfo.partialConflict; - } - return false; + Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId()); + return conflicts != null && conflicts.getOrDefault(schedule, false); } /** @@ -816,17 +813,15 @@ public class DvrScheduleManager { @VisibleForTesting static List<ScheduledRecording> getConflictingSchedules( List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) { - List<ScheduledRecording> result = new ArrayList<>(); - for (ConflictInfo conflictInfo : - getConflictingSchedulesInfo(schedules, tunerCount, periods)) { - result.add(conflictInfo.schedule); - } + List<ScheduledRecording> result = new ArrayList<>( + getConflictingSchedulesInfo(schedules, tunerCount, periods).keySet()); + Collections.sort(result, RESULT_COMPARATOR); return result; } @VisibleForTesting - static List<ConflictInfo> getConflictingSchedulesInfo(List<ScheduledRecording> schedules, - int tunerCount) { + static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo( + List<ScheduledRecording> schedules, int tunerCount) { return getConflictingSchedulesInfo(schedules, tunerCount, null); } @@ -841,13 +836,13 @@ public class DvrScheduleManager { * to be partially recorded under the given schedules and tuner count {@code true}, * or not {@code false}. */ - private static List<ConflictInfo> getConflictingSchedulesInfo( + private static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo( List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) { List<ScheduledRecording> schedulesToCheck = new ArrayList<>(schedules); // Sort by the same order as that in InputTaskScheduler. Collections.sort(schedulesToCheck, InputTaskScheduler.getRecordingOrderComparator()); List<ScheduledRecording> recordings = new ArrayList<>(); - Map<ScheduledRecording, ConflictInfo> conflicts = new HashMap<>(); + Map<ScheduledRecording, Boolean> conflicts = new HashMap<>(); Map<ScheduledRecording, ScheduledRecording> modified2OriginalSchedules = new HashMap<>(); // Simulate InputTaskScheduler. while (!schedulesToCheck.isEmpty()) { @@ -858,29 +853,26 @@ public class DvrScheduleManager { if (modified2OriginalSchedules.containsKey(schedule)) { // Schedule has been modified, which means it's already conflicted. // Modify its state to partially conflicted. - ScheduledRecording originalSchedule = modified2OriginalSchedules.get(schedule); - conflicts.put(originalSchedule, new ConflictInfo(originalSchedule, true)); + conflicts.put(modified2OriginalSchedules.get(schedule), true); } } else { ScheduledRecording candidate = findReplaceableRecording(recordings, schedule); if (candidate != null) { if (!modified2OriginalSchedules.containsKey(candidate)) { - conflicts.put(candidate, new ConflictInfo(candidate, true)); + conflicts.put(candidate, true); } recordings.remove(candidate); recordings.add(schedule); if (modified2OriginalSchedules.containsKey(schedule)) { // Schedule has been modified, which means it's already conflicted. // Modify its state to partially conflicted. - ScheduledRecording originalSchedule = - modified2OriginalSchedules.get(schedule); - conflicts.put(originalSchedule, new ConflictInfo(originalSchedule, true)); + conflicts.put(modified2OriginalSchedules.get(schedule), true); } } else { if (!modified2OriginalSchedules.containsKey(schedule)) { // if schedule has been modified, it's already conflicted. // No need to add it again. - conflicts.put(schedule, new ConflictInfo(schedule, false)); + conflicts.put(schedule, false); } long earliestEndTime = getEarliestEndTime(recordings); if (earliestEndTime < schedule.getEndTimeMs()) { @@ -920,14 +912,7 @@ public class DvrScheduleManager { } } } - List<ConflictInfo> result = new ArrayList<>(conflicts.values()); - Collections.sort(result, new Comparator<ConflictInfo>() { - @Override - public int compare(ConflictInfo lhs, ConflictInfo rhs) { - return RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule); - } - }); - return result; + return conflicts; } private static void removeFinishedRecordings(List<ScheduledRecording> recordings, @@ -969,17 +954,6 @@ public class DvrScheduleManager { return earliest; } - @VisibleForTesting - static class ConflictInfo { - public ScheduledRecording schedule; - public boolean partialConflict; - - ConflictInfo(ScheduledRecording schedule, boolean partialConflict) { - this.schedule = schedule; - this.partialConflict = partialConflict; - } - } - /** * A listener which is notified the initialization of schedule manager. */ @@ -996,9 +970,6 @@ public class DvrScheduleManager { public interface OnConflictStateChangeListener { /** * Called when the conflicting schedules change. - * <p> - * Note that this can be called before - * {@link ScheduledRecordingListener#onScheduledRecordingAdded} is called. * * @param conflict {@code true} if the {@code schedules} are the new conflicts, otherwise * {@code false}. diff --git a/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java b/src/com/android/tv/dvr/DvrStartRecordingReceiver.java index 8c6ee145..6d2f0d43 100644 --- a/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java +++ b/src/com/android/tv/dvr/DvrStartRecordingReceiver.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.recorder; +package com.android.tv.dvr; import com.android.tv.TvApplication; diff --git a/src/com/android/tv/dvr/DvrStorageStatusManager.java b/src/com/android/tv/dvr/DvrStorageStatusManager.java index 2d41d732..a653b5f4 100644 --- a/src/com/android/tv/dvr/DvrStorageStatusManager.java +++ b/src/com/android/tv/dvr/DvrStorageStatusManager.java @@ -25,7 +25,6 @@ import android.content.IntentFilter; import android.content.OperationApplicationException; import android.database.Cursor; import android.media.tv.TvContract; -import android.media.tv.TvInputInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.Environment; @@ -37,11 +36,8 @@ import android.support.annotation.IntDef; import android.support.annotation.WorkerThread; import android.util.Log; -import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.tuner.tvinput.TunerTvInputService; -import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; import java.io.File; @@ -298,7 +294,7 @@ public class DvrStorageStatusManager { storageMounted, storageMountedDir, storageMountedCapacity); } - private class CleanUpDbTask extends AsyncTask<Void, Void, Boolean> { + private class CleanUpDbTask extends AsyncTask<Void, Void, Void> { private final ContentResolver mContentResolver; private CleanUpDbTask() { @@ -306,15 +302,13 @@ public class DvrStorageStatusManager { } @Override - protected Boolean doInBackground(Void... params) { + protected Void doInBackground(Void... params) { @DvrStorageStatusManager.StorageStatus int storageStatus = getDvrStorageStatus(); if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { return null; } - if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) { - return true; - } - List<ContentProviderOperation> ops = getDeleteOps(); + List<ContentProviderOperation> ops = getDeleteOps(storageStatus + == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL); if (ops == null || ops.isEmpty()) { return null; } @@ -335,28 +329,13 @@ public class DvrStorageStatusManager { } @Override - protected void onPostExecute(Boolean forgetStorage) { - if (forgetStorage != null && forgetStorage == true) { - DvrManager dvrManager = TvApplication.getSingletons(mContext).getDvrManager(); - TvInputManagerHelper tvInputManagerHelper = - TvApplication.getSingletons(mContext).getTvInputManagerHelper(); - List<TvInputInfo> tvInputInfoList = - tvInputManagerHelper.getTvInputInfos(true, false); - if (tvInputInfoList == null || tvInputInfoList.isEmpty()) { - return; - } - for (TvInputInfo info : tvInputInfoList) { - if (Utils.isBundledInput(info.getId())) { - dvrManager.forgetStorage(info.getId()); - } - } - } + protected void onPostExecute(Void result) { if (mCleanUpDbTask == this) { mCleanUpDbTask = null; } } - private List<ContentProviderOperation> getDeleteOps() { + private List<ContentProviderOperation> getDeleteOps(boolean deleteAll) { List<ContentProviderOperation> ops = new ArrayList<>(); try (Cursor c = mContentResolver.query( @@ -385,7 +364,7 @@ public class DvrStorageStatusManager { continue; } File recordedProgramDir = new File(dataUri.getPath()); - if (!recordedProgramDir.exists()) { + if (deleteAll || !recordedProgramDir.exists()) { ops.add(ContentProviderOperation.newDelete( TvContract.buildRecordedProgramUri(Long.parseLong(id))).build()); } diff --git a/src/com/android/tv/dvr/ui/DvrUiHelper.java b/src/com/android/tv/dvr/DvrUiHelper.java index 507db6e7..c0d3b0c5 100644 --- a/src/com/android/tv/dvr/ui/DvrUiHelper.java +++ b/src/com/android/tv/dvr/DvrUiHelper.java @@ -14,21 +14,19 @@ * limitations under the License. */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr; import android.annotation.TargetApi; import android.app.Activity; -import android.app.ProgressDialog; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.media.tv.TvInputManager; import android.os.Build; import android.os.Bundle; import android.support.annotation.MainThread; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.ActivityOptionsCompat; +import android.text.TextUtils; import android.widget.ImageView; import android.widget.Toast; @@ -38,36 +36,32 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.Program; -import com.android.tv.dialog.HalfSizedDialogFragment; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrStorageStatusManager; -import com.android.tv.dvr.data.RecordedProgram; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.SeriesRecording; -import com.android.tv.dvr.provider.EpisodicProgramLoadTask; +import com.android.tv.dvr.ui.DvrDetailsActivity; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyScheduledDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelRecordDurationOptionDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelWatchConflictDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrInsufficientSpaceErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrNoFreeSpaceErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrProgramConflictDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrScheduleDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrSmallSizedStorageErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrStopRecordingDialogFragment; -import com.android.tv.dvr.ui.browse.DvrBrowseActivity; -import com.android.tv.dvr.ui.browse.DvrDetailsActivity; -import com.android.tv.dvr.ui.list.DvrSchedulesActivity; +import com.android.tv.dvr.ui.DvrSchedulesActivity; +import com.android.tv.dvr.ui.DvrSeriesDeletionActivity; +import com.android.tv.dvr.ui.DvrSeriesScheduledDialogActivity; +import com.android.tv.dvr.ui.DvrSeriesSettingsActivity; +import com.android.tv.dvr.ui.DvrStopRecordingFragment; +import com.android.tv.dvr.ui.DvrStopSeriesRecordingDialogFragment; +import com.android.tv.dvr.ui.DvrStopSeriesRecordingFragment; +import com.android.tv.dvr.ui.HalfSizedDialogFragment; import com.android.tv.dvr.ui.list.DvrSchedulesFragment; import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; -import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; -import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Set; /** * A helper class for DVR UI. @@ -75,54 +69,94 @@ import java.util.Set; @MainThread @TargetApi(Build.VERSION_CODES.N) public class DvrUiHelper { - private static String TAG = "DvrUiHelper"; + /** + * Handles the action to create the new schedule. It returns {@code true} if the schedule is + * added and there's no additional UI, otherwise {@code false}. + */ + public static boolean handleCreateSchedule(MainActivity activity, Program program) { + if (program == null) { + return false; + } + DvrManager dvrManager = TvApplication.getSingletons(activity).getDvrManager(); + if (!program.isEpisodic()) { + // One time recording. + dvrManager.addSchedule(program); + if (!dvrManager.getConflictingSchedules(program).isEmpty()) { + DvrUiHelper.showScheduleConflictDialog(activity, program); + return false; + } + } else { + SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program); + if (seriesRecording == null || seriesRecording.isStopped()) { + DvrUiHelper.showScheduleDialog(activity, program); + return false; + } else { + // Show recorded program rather than the schedule. + RecordedProgram recordedProgram = dvrManager.getRecordedProgram(program.getTitle(), + program.getSeasonNumber(), program.getEpisodeNumber()); + if (recordedProgram != null) { + DvrUiHelper.showAlreadyRecordedDialog(activity, program); + return false; + } + ScheduledRecording duplicate = dvrManager.getScheduledRecording(program.getTitle(), + program.getSeasonNumber(), program.getEpisodeNumber()); + if (duplicate != null + && (duplicate.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED + || duplicate.getState() + == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { + DvrUiHelper.showAlreadyScheduleDialog(activity, program); + return false; + } + // Just add the schedule. + dvrManager.addSchedule(program); + } + } + return true; - private static ProgressDialog sProgressDialog = null; + } /** * Checks if the storage status is good for recording and shows error messages if needed. * - * @param recordingRequestRunnable if the storage status is OK to record or users choose to - * perform the operation anyway, this Runnable will run. + * @return true if the storage status is fine to be recorded for {@code inputId}. */ - public static void checkStorageStatusAndShowErrorMessage(Activity activity, String inputId, - Runnable recordingRequestRunnable) { - if (Utils.isBundledInput(inputId)) { - switch (TvApplication.getSingletons(activity).getDvrStorageStatusManager() - .getDvrStorageStatus()) { - case DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL: - showDvrSmallSizedStorageErrorDialog(activity); - return; - case DvrStorageStatusManager.STORAGE_STATUS_MISSING: - showDvrMissingStorageErrorDialog(activity); - return; - case DvrStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT: - showDvrNoFreeSpaceErrorDialog(activity, recordingRequestRunnable); - return; - } + public static boolean checkStorageStatusAndShowErrorMessage(Activity activity, String inputId) { + if (!Utils.isBundledInput(inputId)) { + return true; + } + DvrStorageStatusManager dvrStorageStatusManager = + TvApplication.getSingletons(activity).getDvrStorageStatusManager(); + int status = dvrStorageStatusManager.getDvrStorageStatus(); + if (status == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) { + showDvrSmallSizedStorageErrorDialog(activity); + return false; + } else if (status == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { + showDvrMissingStorageErrorDialog(activity, inputId); + return false; + } else if (status == DvrStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT) { + // TODO: handle insufficient storage case. + return true; + } else { + return true; } - recordingRequestRunnable.run(); } /** * Shows the schedule dialog. */ - public static void showScheduleDialog(Activity activity, Program program, - boolean addCurrentProgramToSeries) { + public static void showScheduleDialog(MainActivity activity, Program program) { if (SoftPreconditions.checkNotNull(program) == null) { return; } Bundle args = new Bundle(); args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); - args.putBoolean(DvrScheduleFragment.KEY_ADD_CURRENT_PROGRAM_TO_SERIES, - addCurrentProgramToSeries); showDialogFragment(activity, new DvrScheduleDialogFragment(), args, true, true); } /** * Shows the recording duration options dialog. */ - public static void showChannelRecordDurationOptions(Activity activity, Channel channel) { + public static void showChannelRecordDurationOptions(MainActivity activity, Channel channel) { if (SoftPreconditions.checkNotNull(channel) == null) { return; } @@ -134,7 +168,7 @@ public class DvrUiHelper { /** * Shows the dialog which says that the new schedule conflicts with others. */ - public static void showScheduleConflictDialog(Activity activity, Program program) { + public static void showScheduleConflictDialog(MainActivity activity, Program program) { if (program == null) { return; } @@ -158,47 +192,20 @@ public class DvrUiHelper { /** * Shows DVR insufficient space error dialog. */ - public static void showDvrInsufficientSpaceErrorDialog(MainActivity activity, - Set<String> failedScheduledRecordingInfoSet) { - Bundle args = new Bundle(); - ArrayList<String> failedScheduledRecordingInfoArray = - new ArrayList<>(failedScheduledRecordingInfoSet); - args.putStringArrayList(DvrInsufficientSpaceErrorFragment.FAILED_SCHEDULED_RECORDING_INFOS, - failedScheduledRecordingInfoArray); - showDialogFragment(activity, new DvrInsufficientSpaceErrorDialogFragment(), args); + public static void showDvrInsufficientSpaceErrorDialog(MainActivity activity) { + showDialogFragment(activity, new DvrInsufficientSpaceErrorDialogFragment(), null); Utils.clearRecordingFailedReason(activity, TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); - Utils.clearFailedScheduledRecordingInfoSet(activity); - } - - /** - * Shows DVR no free space error dialog. - * - * @param recordingRequestRunnable the recording request to be executed when users choose - * {@link DvrGuidedStepFragment#ACTION_RECORD_ANYWAY}. - */ - public static void showDvrNoFreeSpaceErrorDialog(Activity activity, - Runnable recordingRequestRunnable) { - DvrHalfSizedDialogFragment fragment = new DvrNoFreeSpaceErrorDialogFragment(); - fragment.setOnActionClickListener(new HalfSizedDialogFragment.OnActionClickListener() { - @Override - public void onActionClick(long actionId) { - if (actionId == DvrGuidedStepFragment.ACTION_RECORD_ANYWAY) { - recordingRequestRunnable.run(); - } else if (actionId == DvrGuidedStepFragment.ACTION_DELETE_RECORDINGS) { - Intent intent = new Intent(activity, DvrBrowseActivity.class); - activity.startActivity(intent); - } - } - }); - showDialogFragment(activity, fragment, null); } /** * Shows DVR missing storage error dialog. */ - private static void showDvrMissingStorageErrorDialog(Activity activity) { - showDialogFragment(activity, new DvrMissingStorageErrorDialogFragment(), null); + private static void showDvrMissingStorageErrorDialog(Activity activity, String inputId) { + SoftPreconditions.checkArgument(!TextUtils.isEmpty(inputId)); + Bundle args = new Bundle(); + args.putString(DvrHalfSizedDialogFragment.KEY_INPUT_ID, inputId); + showDialogFragment(activity, new DvrMissingStorageErrorDialogFragment(), args); } /** @@ -224,7 +231,7 @@ public class DvrUiHelper { /** * Shows "already scheduled" dialog. */ - public static void showAlreadyScheduleDialog(Activity activity, Program program) { + public static void showAlreadyScheduleDialog(MainActivity activity, Program program) { if (program == null) { return; } @@ -236,7 +243,7 @@ public class DvrUiHelper { /** * Shows "already recorded" dialog. */ - public static void showAlreadyRecordedDialog(Activity activity, Program program) { + public static void showAlreadyRecordedDialog(MainActivity activity, Program program) { if (program == null) { return; } @@ -245,87 +252,6 @@ public class DvrUiHelper { showDialogFragment(activity, new DvrAlreadyRecordedDialogFragment(), args, false, true); } - /** - * Handle the request of recording a current program. It will handle creating schedules and - * shows the proper dialog and toast message respectively for timed-recording and program - * recording cases. - * - * @param addProgramToSeries denotes whether the program to be recorded should be added into - * the series recording when users choose to record the entire series. - */ - public static void requestRecordingCurrentProgram(Activity activity, - Channel channel, Program program, boolean addProgramToSeries) { - if (program == null) { - DvrUiHelper.showChannelRecordDurationOptions(activity, channel); - } else if (DvrUiHelper.handleCreateSchedule(activity, program, addProgramToSeries)) { - String msg = activity.getString(R.string.dvr_msg_current_program_scheduled, - program.getTitle(), Utils.toTimeString(program.getEndTimeUtcMillis(), false)); - Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show(); - } - } - - /** - * Handle the request of recording a future program. It will handle creating schedules and - * shows the proper toast message. - * - * @param addProgramToSeries denotes whether the program to be recorded should be added into - * the series recording when users choose to record the entire series. - */ - public static void requestRecordingFutureProgram(Activity activity, - Program program, boolean addProgramToSeries) { - if (DvrUiHelper.handleCreateSchedule(activity, program, addProgramToSeries)) { - String msg = activity.getString( - R.string.dvr_msg_program_scheduled, program.getTitle()); - ToastUtils.show(activity, msg, Toast.LENGTH_SHORT); - } - } - - /** - * Handles the action to create the new schedule. It returns {@code true} if the schedule is - * added and there's no additional UI, otherwise {@code false}. - */ - private static boolean handleCreateSchedule(Activity activity, Program program, - boolean addProgramToSeries) { - if (program == null) { - return false; - } - DvrManager dvrManager = TvApplication.getSingletons(activity).getDvrManager(); - if (!program.isEpisodic()) { - // One time recording. - dvrManager.addSchedule(program); - if (!dvrManager.getConflictingSchedules(program).isEmpty()) { - DvrUiHelper.showScheduleConflictDialog(activity, program); - return false; - } - } else { - // Show recorded program rather than the schedule. - RecordedProgram recordedProgram = dvrManager.getRecordedProgram(program.getTitle(), - program.getSeasonNumber(), program.getEpisodeNumber()); - if (recordedProgram != null) { - DvrUiHelper.showAlreadyRecordedDialog(activity, program); - return false; - } - ScheduledRecording duplicate = dvrManager.getScheduledRecording(program.getTitle(), - program.getSeasonNumber(), program.getEpisodeNumber()); - if (duplicate != null - && (duplicate.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED - || duplicate.getState() - == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { - DvrUiHelper.showAlreadyScheduleDialog(activity, program); - return false; - } - SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program); - if (seriesRecording == null || seriesRecording.isStopped()) { - DvrUiHelper.showScheduleDialog(activity, program, addProgramToSeries); - return false; - } else { - // Just add the schedule. - dvrManager.addSchedule(program); - } - } - return true; - } - private static void showDialogFragment(Activity activity, DvrHalfSizedDialogFragment dialogFragment, Bundle args) { showDialogFragment(activity, dialogFragment, args, false, false); @@ -415,66 +341,19 @@ public class DvrUiHelper { /** * Shows the series settings activity. * - * @param programs list of programs which belong to the series. + * @param channelIds Channel ID list which has programs belonging to the series. */ public static void startSeriesSettingsActivity(Context context, long seriesRecordingId, - @Nullable List<Program> programs, boolean removeEmptySeriesSchedule, - boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog, - Program currentProgram) { - SeriesRecording series = TvApplication.getSingletons(context).getDvrDataManager() - .getSeriesRecording(seriesRecordingId); - if (series == null) { - return; - } - if (programs != null) { - startSeriesSettingsActivityInternal(context, seriesRecordingId, programs, - removeEmptySeriesSchedule, isWindowTranslucent, - showViewScheduleOptionInDialog, currentProgram); - } else { - EpisodicProgramLoadTask episodicProgramLoadTask = - new EpisodicProgramLoadTask(context, series) { - @Override - protected void onPostExecute(List<Program> loadedPrograms) { - sProgressDialog.dismiss(); - sProgressDialog = null; - startSeriesSettingsActivityInternal(context, seriesRecordingId, - loadedPrograms == null ? Collections.EMPTY_LIST : loadedPrograms, - removeEmptySeriesSchedule, isWindowTranslucent, - showViewScheduleOptionInDialog, currentProgram); - } - }.setLoadCurrentProgram(true) - .setLoadDisallowedProgram(true) - .setLoadScheduledEpisode(true) - .setIgnoreChannelOption(true); - sProgressDialog = ProgressDialog.show(context, null, context.getString( - R.string.dvr_series_progress_message_reading_programs), true, true, - new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialogInterface) { - episodicProgramLoadTask.cancel(true); - sProgressDialog = null; - } - }); - episodicProgramLoadTask.execute(); - } - } - - private static void startSeriesSettingsActivityInternal(Context context, long seriesRecordingId, - @NonNull List<Program> programs, boolean removeEmptySeriesSchedule, - boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog, - Program currentProgram) { - SoftPreconditions.checkState(programs != null, - TAG, "Start series settings activity but programs is null"); + @Nullable long[] channelIds, boolean removeEmptySeriesSchedule, + boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog) { Intent intent = new Intent(context, DvrSeriesSettingsActivity.class); intent.putExtra(DvrSeriesSettingsActivity.SERIES_RECORDING_ID, seriesRecordingId); - BigArguments.reset(); - BigArguments.setArgument(DvrSeriesSettingsActivity.PROGRAM_LIST, programs); + intent.putExtra(DvrSeriesSettingsActivity.CHANNEL_ID_LIST, channelIds); intent.putExtra(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING, removeEmptySeriesSchedule); intent.putExtra(DvrSeriesSettingsActivity.IS_WINDOW_TRANSLUCENT, isWindowTranslucent); intent.putExtra(DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG, showViewScheduleOptionInDialog); - intent.putExtra(DvrSeriesSettingsActivity.CURRENT_PROGRAM, currentProgram); context.startActivity(intent); } @@ -482,8 +361,7 @@ public class DvrUiHelper { * Shows "series recording scheduled" dialog activity. */ public static void StartSeriesScheduledDialogActivity(Context context, - SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog, - List<Program> programs) { + SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog) { if (seriesRecording == null) { return; } @@ -492,9 +370,6 @@ public class DvrUiHelper { seriesRecording.getId()); intent.putExtra(DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION, showViewScheduleOptionInDialog); - BigArguments.reset(); - BigArguments.setArgument(DvrSeriesScheduledFragment.SERIES_SCHEDULED_KEY_PROGRAMS, - programs); context.startActivity(intent); } @@ -572,4 +447,4 @@ public class DvrUiHelper { Utils.toTimeString(endTimeMs, false)); Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/DvrWatchedPositionManager.java b/src/com/android/tv/dvr/DvrWatchedPositionManager.java index dc05ed06..4eada742 100644 --- a/src/com/android/tv/dvr/DvrWatchedPositionManager.java +++ b/src/com/android/tv/dvr/DvrWatchedPositionManager.java @@ -22,7 +22,6 @@ import android.media.tv.TvInputManager; import android.support.annotation.IntDef; import com.android.tv.common.SharedPreferencesUtils; -import com.android.tv.dvr.data.RecordedProgram; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/EpisodicProgramLoadTask.java index ba0aca51..15ca2700 100644 --- a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java +++ b/src/com/android/tv/dvr/EpisodicProgramLoadTask.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr.provider; +package com.android.tv.dvr; import android.annotation.TargetApi; import android.content.Context; @@ -24,15 +24,13 @@ import android.media.tv.TvContract.Programs; import android.net.Uri; import android.os.Build; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; +import android.text.TextUtils; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.data.SeasonEpisodeNumber; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.AsyncDbTask.AsyncProgramQueryTask; import com.android.tv.util.AsyncDbTask.CursorFilter; import com.android.tv.util.PermissionUtils; @@ -42,6 +40,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; /** @@ -254,13 +253,21 @@ abstract public class EpisodicProgramLoadTask { return sqlParams; } + @VisibleForTesting + static boolean isEpisodeScheduled(Collection<ScheduledEpisode> scheduledEpisodes, + ScheduledEpisode episode) { + // The episode whose season number or episode number is null will always be scheduled. + return scheduledEpisodes.contains(episode) && !TextUtils.isEmpty(episode.seasonNumber) + && !TextUtils.isEmpty(episode.episodeNumber); + } + /** * Filter the programs which match the series recording. The episodes which the schedules are * already created for are filtered out too. */ private class SeriesRecordingCursorFilter implements CursorFilter { private final Set<Long> mDisallowedProgramIds = new HashSet<>(); - private final Set<SeasonEpisodeNumber> mSeasonEpisodeNumbers = new HashSet<>(); + private final Set<ScheduledEpisode> mScheduledEpisodes = new HashSet<>(); SeriesRecordingCursorFilter(List<SeriesRecording> seriesRecordings) { if (!mLoadDisallowedProgram) { @@ -275,7 +282,7 @@ abstract public class EpisodicProgramLoadTask { if (seriesRecordingIds.contains(r.getSeriesRecordingId()) && r.getState() != ScheduledRecording.STATE_RECORDING_FAILED && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) { - mSeasonEpisodeNumbers.add(new SeasonEpisodeNumber(r)); + mScheduledEpisodes.add(new ScheduledEpisode(r)); } } } @@ -299,9 +306,9 @@ abstract public class EpisodicProgramLoadTask { } if (programMatches) { return mLoadScheduledEpisode - || !mSeasonEpisodeNumbers.contains(new SeasonEpisodeNumber( - seriesRecording.getId(), program.getSeasonNumber(), - program.getEpisodeNumber())); + || !isEpisodeScheduled(mScheduledEpisodes, new ScheduledEpisode( + seriesRecording.getId(), program.getSeasonNumber(), + program.getEpisodeNumber())); } } return false; @@ -326,4 +333,50 @@ abstract public class EpisodicProgramLoadTask { public String[] selectionArgs; public CursorFilter filter; } + + /** + * A plain java object which includes the season/episode number for the series recording. + */ + public static class ScheduledEpisode { + public final long seriesRecordingId; + public final String seasonNumber; + public final String episodeNumber; + + /** + * Create a new Builder with the values set from an existing {@link ScheduledRecording}. + */ + ScheduledEpisode(ScheduledRecording r) { + this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber()); + } + + public ScheduledEpisode(long seriesRecordingId, String seasonNumber, String episodeNumber) { + this.seriesRecordingId = seriesRecordingId; + this.seasonNumber = seasonNumber; + this.episodeNumber = episodeNumber; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ScheduledEpisode)) return false; + ScheduledEpisode that = (ScheduledEpisode) o; + return seriesRecordingId == that.seriesRecordingId + && Objects.equals(seasonNumber, that.seasonNumber) + && Objects.equals(episodeNumber, that.episodeNumber); + } + + @Override + public int hashCode() { + return Objects.hash(seriesRecordingId, seasonNumber, episodeNumber); + } + + @Override + public String toString() { + return "ScheduledEpisode{" + + "seriesRecordingId=" + seriesRecordingId + + ", seasonNumber='" + seasonNumber + + ", episodeNumber=" + episodeNumber + + '}'; + } + } } diff --git a/src/com/android/tv/dvr/data/IdGenerator.java b/src/com/android/tv/dvr/IdGenerator.java index 2ade1dad..0ed6362c 100644 --- a/src/com/android/tv/dvr/data/IdGenerator.java +++ b/src/com/android/tv/dvr/IdGenerator.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr.data; +package com.android.tv.dvr; import java.util.concurrent.atomic.AtomicLong; diff --git a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java b/src/com/android/tv/dvr/InputTaskScheduler.java index 46546a76..53c89ebc 100644 --- a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java +++ b/src/com/android/tv/dvr/InputTaskScheduler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr.recorder; +package com.android.tv.dvr; import android.content.Context; import android.media.tv.TvInputInfo; @@ -30,10 +30,6 @@ import android.util.LongSparseArray; import com.android.tv.InputSessionManager; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.WritableDvrDataManager; -import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Clock; import com.android.tv.util.CompositeComparator; diff --git a/src/com/android/tv/dvr/data/RecordedProgram.java b/src/com/android/tv/dvr/RecordedProgram.java index 18e1c769..dd744f80 100644 --- a/src/com/android/tv/dvr/data/RecordedProgram.java +++ b/src/com/android/tv/dvr/RecordedProgram.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.data; +package com.android.tv.dvr; import static android.media.tv.TvContract.RecordedPrograms; diff --git a/src/com/android/tv/dvr/recorder/RecordingTask.java b/src/com/android/tv/dvr/RecordingTask.java index c3314dde..c3d236b0 100644 --- a/src/com/android/tv/dvr/recorder/RecordingTask.java +++ b/src/com/android/tv/dvr/RecordingTask.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.recorder; +package com.android.tv.dvr; import android.annotation.TargetApi; import android.content.Context; @@ -37,10 +37,7 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.WritableDvrDataManager; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.recorder.InputTaskScheduler.HandlerWrapper; +import com.android.tv.dvr.InputTaskScheduler.HandlerWrapper; import com.android.tv.util.Clock; import com.android.tv.util.Utils; @@ -259,21 +256,13 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback public void run() { if (TvApplication.getSingletons(mContext).getMainActivityWrapper() .isResumed()) { - ScheduledRecording scheduledRecording = mDataManager - .getScheduledRecording(mScheduledRecording.getId()); - if (scheduledRecording != null) { - Toast.makeText(mContext.getApplicationContext(), - mContext.getString(R.string - .dvr_error_insufficient_space_description_one_recording, - scheduledRecording.getProgramDisplayTitle(mContext)), - Toast.LENGTH_LONG) - .show(); - } + Toast.makeText(mContext.getApplicationContext(), + R.string.dvr_error_insufficient_space_description, + Toast.LENGTH_LONG) + .show(); } else { Utils.setRecordingFailedReason(mContext.getApplicationContext(), TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); - Utils.addFailedScheduledRecordingInfo(mContext.getApplicationContext(), - mScheduledRecording.getProgramDisplayTitle(mContext)); } } }); diff --git a/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java b/src/com/android/tv/dvr/ScheduledProgramReaper.java index d958c4a1..cd79a631 100644 --- a/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java +++ b/src/com/android/tv/dvr/ScheduledProgramReaper.java @@ -14,14 +14,11 @@ * limitations under the License. */ -package com.android.tv.dvr.recorder; +package com.android.tv.dvr; import android.support.annotation.MainThread; import android.support.annotation.VisibleForTesting; -import com.android.tv.dvr.WritableDvrDataManager; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.Clock; import java.util.ArrayList; diff --git a/src/com/android/tv/dvr/data/ScheduledRecording.java b/src/com/android/tv/dvr/ScheduledRecording.java index 88849a2c..2bda10ea 100644 --- a/src/com/android/tv/dvr/data/ScheduledRecording.java +++ b/src/com/android/tv/dvr/ScheduledRecording.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr.data; +package com.android.tv.dvr; import android.content.ContentValues; import android.content.Context; @@ -27,11 +27,9 @@ import android.text.TextUtils; import android.util.Range; import com.android.tv.R; -import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.Program; -import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.provider.DvrContract.Schedules; import com.android.tv.util.CompositeComparator; import com.android.tv.util.Utils; @@ -685,19 +683,6 @@ public final class ScheduledRecording implements Parcelable { } } - /** - * Returns the program's display title, if the program title is not null, returns program title. - * Otherwise returns the channel name. - */ - public String getProgramDisplayTitle(Context context) { - if (!TextUtils.isEmpty(mProgramTitle)) { - return mProgramTitle; - } - Channel channel = TvApplication.getSingletons(context).getChannelDataManager() - .getChannel(mChannelId); - return channel != null ? channel.getDisplayName() - : context.getString(R.string.no_program_information); - } /** * Converts a string to a @RecordingType int, defaulting to {@link #TYPE_TIMED}. diff --git a/src/com/android/tv/dvr/recorder/Scheduler.java b/src/com/android/tv/dvr/Scheduler.java index 19e73342..ce78e1be 100644 --- a/src/com/android/tv/dvr/recorder/Scheduler.java +++ b/src/com/android/tv/dvr/Scheduler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr.recorder; +package com.android.tv.dvr; import android.app.AlarmManager; import android.app.PendingIntent; @@ -32,12 +32,8 @@ import android.util.Range; import com.android.tv.InputSessionManager; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.ChannelDataManager.Listener; -import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.WritableDvrDataManager; -import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Clock; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; diff --git a/src/com/android/tv/dvr/data/SeriesInfo.java b/src/com/android/tv/dvr/SeriesInfo.java index a0dec4a4..30256dc5 100644 --- a/src/com/android/tv/dvr/data/SeriesInfo.java +++ b/src/com/android/tv/dvr/SeriesInfo.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.data; +package com.android.tv.dvr; /** * Series information. diff --git a/src/com/android/tv/dvr/data/SeriesRecording.java b/src/com/android/tv/dvr/SeriesRecording.java index b7cf0f66..f0690f5f 100644 --- a/src/com/android/tv/dvr/data/SeriesRecording.java +++ b/src/com/android/tv/dvr/SeriesRecording.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr.data; +package com.android.tv.dvr; import android.content.ContentValues; import android.database.Cursor; @@ -26,7 +26,6 @@ import android.text.TextUtils; import com.android.tv.data.BaseProgram; import com.android.tv.data.Program; -import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.provider.DvrContract.SeriesRecordings; import com.android.tv.util.Utils; diff --git a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/SeriesRecordingScheduler.java index 8a211f66..5ed12ce8 100644 --- a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java +++ b/src/com/android/tv/dvr/SeriesRecordingScheduler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr.recorder; +package com.android.tv.dvr; import android.annotation.SuppressLint; import android.annotation.TargetApi; @@ -23,6 +23,7 @@ import android.content.SharedPreferences; import android.os.AsyncTask; import android.os.Build; import android.support.annotation.MainThread; +import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; @@ -35,16 +36,9 @@ import com.android.tv.common.SharedPreferencesUtils; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; import com.android.tv.data.epg.EpgFetcher; -import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.WritableDvrDataManager; -import com.android.tv.dvr.data.SeasonEpisodeNumber; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.SeriesInfo; -import com.android.tv.dvr.data.SeriesRecording; -import com.android.tv.dvr.provider.EpisodicProgramLoadTask; +import com.android.tv.dvr.EpisodicProgramLoadTask.ScheduledEpisode; import com.android.tv.experiments.Experiments; import java.util.ArrayList; @@ -58,11 +52,11 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.Set; /** - * Creates the {@link com.android.tv.dvr.data.ScheduledRecording}s for - * the {@link com.android.tv.dvr.data.SeriesRecording}. + * Creates the {@link ScheduledRecording}s for the {@link SeriesRecording}. * <p> * The current implementation assumes that the series recordings are scheduled only for one channel. */ @@ -91,13 +85,15 @@ public class SeriesRecordingScheduler { private final DvrManager mDvrManager; private final WritableDvrDataManager mDataManager; private final List<SeriesRecordingUpdateTask> mScheduleTasks = new ArrayList<>(); - private final LongSparseArray<FetchSeriesInfoTask> mFetchSeriesInfoTasks = - new LongSparseArray<>(); + private final List<FetchSeriesInfoTask> mFetchSeriesInfoTasks = new ArrayList<>(); private final Set<String> mFetchedSeriesIds = new ArraySet<>(); private final SharedPreferences mSharedPreferences; private boolean mStarted; private boolean mPaused; private final Set<Long> mPendingSeriesRecordings = new ArraySet<>(); + private final Set<OnSeriesRecordingUpdatedListener> mOnSeriesRecordingUpdatedListeners = + new CopyOnWriteArraySet<>(); + private final SeriesRecordingListener mSeriesRecordingListener = new SeriesRecordingListener() { @Override @@ -111,7 +107,7 @@ public class SeriesRecordingScheduler { public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { // Cancel the update. for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator(); - iter.hasNext(); ) { + iter.hasNext(); ) { SeriesRecordingUpdateTask task = iter.next(); if (CollectionUtils.subtract(task.getSeriesRecordings(), seriesRecordings, SeriesRecording.ID_COMPARATOR).isEmpty()) { @@ -119,13 +115,6 @@ public class SeriesRecordingScheduler { iter.remove(); } } - for (SeriesRecording seriesRecording : seriesRecordings) { - FetchSeriesInfoTask task = mFetchSeriesInfoTasks.get(seriesRecording.getId()); - if (task != null) { - task.cancel(true); - mFetchSeriesInfoTasks.remove(seriesRecording.getId()); - } - } } @Override @@ -237,8 +226,7 @@ public class SeriesRecordingScheduler { } if (DEBUG) Log.d(TAG, "stop"); mStarted = false; - for (int i = 0; i < mFetchSeriesInfoTasks.size(); i++) { - FetchSeriesInfoTask task = mFetchSeriesInfoTasks.get(mFetchSeriesInfoTasks.keyAt(i)); + for (FetchSeriesInfoTask task : mFetchSeriesInfoTasks) { task.cancel(true); } mFetchSeriesInfoTasks.clear(); @@ -262,7 +250,7 @@ public class SeriesRecordingScheduler { if (Experiments.CLOUD_EPG.get()) { FetchSeriesInfoTask task = new FetchSeriesInfoTask(seriesRecording); task.execute(); - mFetchSeriesInfoTasks.put(seriesRecording.getId(), task); + mFetchSeriesInfoTasks.add(task); } } @@ -375,6 +363,20 @@ public class SeriesRecordingScheduler { } } + /** + * Adds {@link OnSeriesRecordingUpdatedListener}. + */ + public void addOnSeriesRecordingUpdatedListener(OnSeriesRecordingUpdatedListener listener) { + mOnSeriesRecordingUpdatedListeners.add(listener); + } + + /** + * Removes {@link OnSeriesRecordingUpdatedListener}. + */ + public void removeOnSeriesRecordingUpdatedListener(OnSeriesRecordingUpdatedListener listener) { + mOnSeriesRecordingUpdatedListeners.remove(listener); + } + private boolean needToReadAllChannels(List<SeriesRecording> seriesRecordingsToUpdate) { for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) { if (seriesRecording.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ALL) { @@ -401,7 +403,8 @@ public class SeriesRecordingScheduler { /** * @see #pickOneProgramPerEpisode(List, List) */ - public static LongSparseArray<List<Program>> pickOneProgramPerEpisode( + @VisibleForTesting + static LongSparseArray<List<Program>> pickOneProgramPerEpisode( DvrDataManager dataManager, List<SeriesRecording> seriesRecordings, List<Program> programs) { // Initialize. @@ -412,7 +415,7 @@ public class SeriesRecordingScheduler { seriesRecordingIds.put(seriesRecording.getSeriesId(), seriesRecording.getId()); } // Group programs by the episode. - Map<SeasonEpisodeNumber, List<Program>> programsForEpisodeMap = new HashMap<>(); + Map<ScheduledEpisode, List<Program>> programsForEpisodeMap = new HashMap<>(); for (Program program : programs) { long seriesRecordingId = seriesRecordingIds.get(program.getSeriesId()); if (TextUtils.isEmpty(program.getSeasonNumber()) @@ -421,17 +424,17 @@ public class SeriesRecordingScheduler { result.get(seriesRecordingId).add(program); continue; } - SeasonEpisodeNumber seasonEpisodeNumber = new SeasonEpisodeNumber(seriesRecordingId, + ScheduledEpisode episode = new ScheduledEpisode(seriesRecordingId, program.getSeasonNumber(), program.getEpisodeNumber()); - List<Program> programsForEpisode = programsForEpisodeMap.get(seasonEpisodeNumber); + List<Program> programsForEpisode = programsForEpisodeMap.get(episode); if (programsForEpisode == null) { programsForEpisode = new ArrayList<>(); - programsForEpisodeMap.put(seasonEpisodeNumber, programsForEpisode); + programsForEpisodeMap.put(episode, programsForEpisode); } programsForEpisode.add(program); } // Pick one program. - for (Entry<SeasonEpisodeNumber, List<Program>> entry : programsForEpisodeMap.entrySet()) { + for (Entry<ScheduledEpisode, List<Program>> entry : programsForEpisodeMap.entrySet()) { List<Program> programsForEpisode = entry.getValue(); Collections.sort(programsForEpisode, new Comparator<Program>() { @Override @@ -509,6 +512,13 @@ public class SeriesRecordingScheduler { mDvrManager.addScheduleToSeriesRecording(seriesRecording, programsToSchedule); } } + if (!mOnSeriesRecordingUpdatedListeners.isEmpty()) { + for (OnSeriesRecordingUpdatedListener listener + : mOnSeriesRecordingUpdatedListeners) { + listener.onSeriesRecordingUpdated( + SeriesRecording.toArray(getSeriesRecordings())); + } + } } @Override @@ -551,12 +561,19 @@ public class SeriesRecordingScheduler { mFetchedSeriesIds.add(seriesInfo.getId()); updateFetchedSeries(); } - mFetchSeriesInfoTasks.remove(mSeriesRecording.getId()); + mFetchSeriesInfoTasks.remove(this); } @Override protected void onCancelled(SeriesInfo seriesInfo) { - mFetchSeriesInfoTasks.remove(mSeriesRecording.getId()); + mFetchSeriesInfoTasks.remove(this); } } + + /** + * A listener to notify when series recording are updated. + */ + public interface OnSeriesRecordingUpdatedListener { + void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings); + } } diff --git a/src/com/android/tv/dvr/WritableDvrDataManager.java b/src/com/android/tv/dvr/WritableDvrDataManager.java index 129ba153..bf72d912 100644 --- a/src/com/android/tv/dvr/WritableDvrDataManager.java +++ b/src/com/android/tv/dvr/WritableDvrDataManager.java @@ -18,9 +18,7 @@ package com.android.tv.dvr; import android.support.annotation.MainThread; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.ScheduledRecording.RecordingState; -import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ScheduledRecording.RecordingState; /** * Full data manager. @@ -29,7 +27,7 @@ import com.android.tv.dvr.data.SeriesRecording; * for internal use only. Do not call them from UI directly. */ @MainThread -public interface WritableDvrDataManager extends DvrDataManager { +interface WritableDvrDataManager extends DvrDataManager { /** * Adds new recordings. */ diff --git a/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java b/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java deleted file mode 100644 index 89533dbb..00000000 --- a/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.data; - -import android.text.TextUtils; - -import java.util.Objects; - -/** - * A plain java object which includes the season/episode number for the series recording. - */ -public class SeasonEpisodeNumber { - public final long seriesRecordingId; - public final String seasonNumber; - public final String episodeNumber; - - /** - * Creates a new Builder with the values set from an existing {@link ScheduledRecording}. - */ - public SeasonEpisodeNumber(ScheduledRecording r) { - this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber()); - } - - public SeasonEpisodeNumber(long seriesRecordingId, String seasonNumber, String episodeNumber) { - this.seriesRecordingId = seriesRecordingId; - this.seasonNumber = seasonNumber; - this.episodeNumber = episodeNumber; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof SeasonEpisodeNumber) - || TextUtils.isEmpty(seasonNumber) || TextUtils.isEmpty(episodeNumber)) { - return false; - } - SeasonEpisodeNumber that = (SeasonEpisodeNumber) o; - return seriesRecordingId == that.seriesRecordingId - && Objects.equals(seasonNumber, that.seasonNumber) - && Objects.equals(episodeNumber, that.episodeNumber); - } - - @Override - public int hashCode() { - return Objects.hash(seriesRecordingId, seasonNumber, episodeNumber); - } - - @Override - public String toString() { - return "SeasonEpisodeNumber{" + - "seriesRecordingId=" + seriesRecordingId + - ", seasonNumber='" + seasonNumber + - ", episodeNumber=" + episodeNumber + - '}'; - } -}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java index c5383d02..1a12fb23 100644 --- a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java +++ b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java @@ -21,8 +21,8 @@ import android.database.Cursor; import android.os.AsyncTask; import android.support.annotation.Nullable; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.SeriesRecording; import com.android.tv.dvr.provider.DvrContract.Schedules; import com.android.tv.dvr.provider.DvrContract.SeriesRecordings; import com.android.tv.util.NamedThreadFactory; diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java index 8b9481a9..2f16ba5d 100644 --- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java +++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java @@ -27,8 +27,8 @@ import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.SeriesRecording; import com.android.tv.dvr.provider.DvrContract.Schedules; import com.android.tv.dvr.provider.DvrContract.SeriesRecordings; diff --git a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/ActionPresenterSelector.java index 38a78f5d..8b8cd5c5 100644 --- a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java +++ b/src/com/android/tv/dvr/ui/ActionPresenterSelector.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; import android.graphics.drawable.Drawable; import android.support.v17.leanback.R; @@ -110,7 +110,11 @@ class ActionPresenterSelector extends PresenterSelector { .getDimensionPixelSize(R.dimen.lb_action_padding_horizontal); vh.view.setPaddingRelative(padding, 0, padding, 0); } - vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null); + if (vh.mLayoutDirection == View.LAYOUT_DIRECTION_RTL) { + vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, icon, null); + } else { + vh.mButton.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); + } CharSequence line1 = action.getLabel1(); CharSequence line2 = action.getLabel2(); @@ -126,7 +130,7 @@ class ActionPresenterSelector extends PresenterSelector { @Override public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { ActionViewHolder vh = (ActionViewHolder) viewHolder; - vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null); + vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); vh.view.setPadding(0, 0, 0, 0); vh.mAction = null; } diff --git a/src/com/android/tv/dvr/ui/BigArguments.java b/src/com/android/tv/dvr/ui/BigArguments.java deleted file mode 100644 index ec3b5065..00000000 --- a/src/com/android/tv/dvr/ui/BigArguments.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.support.annotation.NonNull; - -import com.android.tv.common.SoftPreconditions; - -import java.util.HashMap; -import java.util.Map; - -/** - * Stores the object to pass through activities/fragments. - */ -public class BigArguments { - private final static String TAG = "BigArguments"; - private static Map<String, Object> sBigArgumentMap = new HashMap<>(); - - /** - * Sets the argument. - */ - public static void setArgument(String name, @NonNull Object value) { - SoftPreconditions.checkState(value != null, TAG, "Set argument, but value is null"); - sBigArgumentMap.put(name, value); - } - - /** - * Returns the argument which is associated to the name. - */ - public static Object getArgument(String name) { - return sBigArgumentMap.get(name); - } - - /** - * Resets the arguments. - */ - public static void reset() { - sBigArgumentMap.clear(); - } -} diff --git a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java deleted file mode 100644 index cddece73..00000000 --- a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Matrix; -import android.graphics.drawable.BitmapDrawable; -import android.transition.ChangeImageTransform; -import android.transition.TransitionValues; -import android.util.AttributeSet; -import android.view.View; -import android.widget.ImageView; -import android.widget.ImageView.ScaleType; - -import com.android.tv.R; - -import java.util.Map; - -/** - * TODO: Remove this class once b/32405620 is fixed. - * This class is for the workaround of b/32405620 and only for the shared element transition between - * {@link com.android.tv.dvr.ui.browse.RecordingCardView} and - * {@link com.android.tv.dvr.ui.browse.DvrDetailsActivity}. - */ -public class ChangeImageTransformWithScaledParent extends ChangeImageTransform { - private static final String PROPNAME_MATRIX = "android:changeImageTransform:matrix"; - - public ChangeImageTransformWithScaledParent(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public void captureStartValues(TransitionValues transitionValues) { - super.captureStartValues(transitionValues); - applyParentScale(transitionValues); - } - - @Override - public void captureEndValues(TransitionValues transitionValues) { - super.captureEndValues(transitionValues); - applyParentScale(transitionValues); - } - - private void applyParentScale(TransitionValues transitionValues) { - View view = transitionValues.view; - Map<String, Object> values = transitionValues.values; - Matrix matrix = (Matrix) values.get(PROPNAME_MATRIX); - if (matrix != null && view.getId() == R.id.details_overview_image - && view instanceof ImageView) { - ImageView imageView = (ImageView) view; - if (imageView.getScaleType() == ScaleType.CENTER_INSIDE - && imageView.getDrawable() instanceof BitmapDrawable) { - Bitmap bitmap = ((BitmapDrawable) imageView.getDrawable()).getBitmap(); - if (bitmap.getWidth() < imageView.getWidth() - && bitmap.getHeight() < imageView.getHeight()) { - float scale = imageView.getContext().getResources().getFraction( - R.fraction.lb_focus_zoom_factor_medium, 1, 1); - matrix.postScale(scale, scale, imageView.getWidth() / 2, - imageView.getHeight() / 2); - } - } - } - } -} diff --git a/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java new file mode 100644 index 00000000..5d8e20ff --- /dev/null +++ b/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java @@ -0,0 +1,59 @@ +/* + * 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.res.Resources; +import android.support.v17.leanback.widget.Action; +import android.support.v17.leanback.widget.OnActionClickedListener; +import android.support.v17.leanback.widget.SparseArrayObjectAdapter; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.dvr.DvrManager; + +/** + * {@link RecordingDetailsFragment} for current recording in DVR. + */ +public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { + private static final int ACTION_STOP_RECORDING = 1; + + @Override + protected SparseArrayObjectAdapter onCreateActionsAdapter() { + SparseArrayObjectAdapter adapter = + new SparseArrayObjectAdapter(new ActionPresenterSelector()); + Resources res = getResources(); + adapter.set(ACTION_STOP_RECORDING, new Action(ACTION_STOP_RECORDING, + res.getString(R.string.epg_dvr_dialog_message_stop_recording), null, + res.getDrawable(R.drawable.lb_ic_stop))); + return adapter; + } + + @Override + protected OnActionClickedListener onCreateOnActionClickedListener() { + return new OnActionClickedListener() { + @Override + public void onActionClicked(Action action) { + if (action.getId() == ACTION_STOP_RECORDING) { + DvrManager dvrManager = TvApplication.getSingletons(getActivity()) + .getDvrManager(); + dvrManager.stopRecording(getRecording()); + } + getActivity().finish(); + } + }; + } +} diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContent.java b/src/com/android/tv/dvr/ui/DetailsContent.java index b43d1f12..19521fca 100644 --- a/src/com/android/tv/dvr/ui/browse/DetailsContent.java +++ b/src/com/android/tv/dvr/ui/DetailsContent.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; import android.media.tv.TvContract; import android.support.annotation.Nullable; @@ -26,7 +26,7 @@ import com.android.tv.data.Channel; /** * A class for details content. */ -class DetailsContent { +public class DetailsContent { /** Constant for invalid time. */ public static final long INVALID_TIME = -1; diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/DetailsContentPresenter.java index a2e3fe16..175f05bc 100644 --- a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java +++ b/src/com/android/tv/dvr/ui/DetailsContentPresenter.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; import android.app.Activity; import android.animation.Animator; @@ -38,14 +38,13 @@ import com.android.tv.util.Utils; /** * An {@link Presenter} for rendering a detailed description of an DVR item. - * Typically this Presenter will be used in a - * {@link android.support.v17.leanback.widget.DetailsOverviewRowPresenter}. + * Typically this Presenter will be used in a {@link DetailsOverviewRowPresenter}. * Most codes of this class is originated from * {@link android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter}. * The latter class are re-used to provide a customized version of * {@link android.support.v17.leanback.widget.DetailsOverviewRow}. */ -class DetailsContentPresenter extends Presenter { +public class DetailsContentPresenter extends Presenter { /** * The ViewHolder for the {@link DetailsContentPresenter}. */ @@ -114,20 +113,6 @@ class DetailsContentPresenter extends Presenter { public ViewHolder(final View view) { super(view); - view.addOnAttachStateChangeListener( - new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - // In case predraw listener was removed in detach, make sure - // we have the proper layout. - addPreDrawListener(); - } - - @Override - public void onViewDetachedFromWindow(View v) { - removePreDrawListener(); - } - }); mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title); mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle); mBody = (TextView) view.findViewById(R.id.dvr_details_description_body); @@ -291,6 +276,22 @@ class DetailsContentPresenter extends Presenter { @Override public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { } + @Override + public void onViewAttachedToWindow(Presenter.ViewHolder holder) { + // In case predraw listener was removed in detach, make sure + // we have the proper layout. + ViewHolder vh = (ViewHolder) holder; + vh.addPreDrawListener(); + super.onViewAttachedToWindow(holder); + } + + @Override + public void onViewDetachedFromWindow(Presenter.ViewHolder holder) { + ViewHolder vh = (ViewHolder) holder; + vh.removePreDrawListener(); + super.onViewDetachedFromWindow(holder); + } + private void setTopMargin(View view, int topMargin) { ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); lp.topMargin = topMargin; diff --git a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java index 82fe9ce3..6714ecd3 100644 --- a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java +++ b/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; import android.app.Activity; import android.graphics.drawable.BitmapDrawable; @@ -26,7 +26,7 @@ import android.support.v17.leanback.app.BackgroundManager; /** * The Background Helper. */ -class DetailsViewBackgroundHelper { +public class DetailsViewBackgroundHelper { // Background delay serves to avoid kicking off expensive bitmap loading // in case multiple backgrounds are set in quick succession. private static final int SET_BACKGROUND_DELAY_MS = 100; diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java b/src/com/android/tv/dvr/ui/DvrActivity.java index 2b3dcb25..45fb1cf1 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java +++ b/src/com/android/tv/dvr/ui/DvrActivity.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; import android.app.Activity; import android.os.Bundle; @@ -25,11 +25,11 @@ import com.android.tv.TvApplication; /** * {@link android.app.Activity} for DVR UI. */ -public class DvrBrowseActivity extends Activity { +public class DvrActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { TvApplication.setCurrentRunningProcess(this, true); super.onCreate(savedInstanceState); setContentView(R.layout.dvr_main); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java index 936e9c31..9df228d1 100644 --- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java +++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java @@ -28,9 +28,11 @@ import android.widget.Toast; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.dvr.RecordedProgram; import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.util.Utils; import java.util.List; diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java index 3c73cb47..78f21784 100644 --- a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java +++ b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java @@ -25,12 +25,15 @@ import android.support.annotation.NonNull; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.text.format.DateUtils; +import android.widget.Toast; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.util.Utils; import java.util.List; diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java index 803d1017..a6dd31d1 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java +++ b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java @@ -14,9 +14,10 @@ * limitations under the License */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; import android.content.Context; +import android.media.tv.TvInputManager.TvInputCallback; import android.os.Bundle; import android.os.Handler; import android.support.v17.leanback.app.BrowseFragment; @@ -24,11 +25,11 @@ 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.Presenter; import android.support.v17.leanback.widget.TitleViewAdapter; +import android.text.TextUtils; import android.util.Log; -import android.view.View; -import android.view.ViewTreeObserver.OnGlobalFocusChangeListener; import com.android.tv.ApplicationSingletons; import com.android.tv.R; @@ -41,10 +42,9 @@ import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.data.RecordedProgram; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.SeriesRecording; -import com.android.tv.dvr.ui.SortedArrayAdapter; +import com.android.tv.dvr.RecordedProgram; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.SeriesRecording; import java.util.ArrayList; import java.util.Arrays; @@ -79,20 +79,6 @@ public class DvrBrowseFragment extends BrowseFragment implements private ClassPresenterSelector mPresenterSelector; private final HashMap<String, RecordedProgram> mSeriesId2LatestProgram = new HashMap<>(); private final Handler mHandler = new Handler(); - private final OnGlobalFocusChangeListener mOnGlobalFocusChangeListener = - new OnGlobalFocusChangeListener() { - @Override - public void onGlobalFocusChanged(View oldFocus, View newFocus) { - if (oldFocus instanceof RecordingCardView) { - ((RecordingCardView) oldFocus).expandTitle(false, true); - } - if (newFocus instanceof RecordingCardView) { - // If the header transition is ongoing, expand cards immediately without - // animation to make a smooth transition. - ((RecordingCardView) newFocus).expandTitle(true, !isInHeadersTransition()); - } - } - }; private final Comparator<Object> RECORDED_PROGRAM_COMPARATOR = new Comparator<Object>() { @Override @@ -142,7 +128,7 @@ public class DvrBrowseFragment extends BrowseFragment implements public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) { if (mScheduleAdapter != null) { for (ScheduledRecording schedule : schedules) { - onScheduledRecordingConflictStatusChanged(schedule); + onScheduledRecordingStatusChanged(schedule); } } } @@ -168,12 +154,16 @@ public class DvrBrowseFragment extends BrowseFragment implements new ScheduledRecordingPresenter(context)) .addClassPresenter(RecordedProgram.class, new RecordedProgramPresenter(context)) .addClassPresenter(SeriesRecording.class, new SeriesRecordingPresenter(context)) - .addClassPresenter(FullScheduleCardHolder.class, - new FullSchedulesCardPresenter(context)); + .addClassPresenter(FullScheduleCardHolder.class, new FullSchedulesCardPresenter()); mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context))); mGenreLabels.add(getString(R.string.dvr_main_others)); - prepareUiElements(); - if (!startBrowseIfDvrInitialized()) { + setupUiElements(); + setupAdapters(); + mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener); + prepareEntranceTransition(); + if (mDvrDataManager.isInitialized()) { + startEntranceTransition(); + } else { if (!mDvrDataManager.isDvrScheduleLoadFinished()) { mDvrDataManager.addDvrScheduleLoadFinishedListener(this); } @@ -184,19 +174,6 @@ public class DvrBrowseFragment extends BrowseFragment implements } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - view.getViewTreeObserver().addOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener); - } - - @Override - public void onDestroyView() { - getView().getViewTreeObserver() - .removeOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener); - super.onDestroyView(); - } - - @Override public void onDestroy() { if (DEBUG) Log.d(TAG, "onDestroy"); mHandler.removeCallbacks(mUpdateRowsRunnable); @@ -218,13 +195,25 @@ public class DvrBrowseFragment extends BrowseFragment implements @Override public void onDvrScheduleLoadFinished() { - startBrowseIfDvrInitialized(); + List<ScheduledRecording> scheduledRecordings = mDvrDataManager.getAllScheduledRecordings(); + onScheduledRecordingAdded(ScheduledRecording.toArray(scheduledRecordings)); + List<SeriesRecording> seriesRecordings = mDvrDataManager.getSeriesRecordings(); + onSeriesRecordingAdded(SeriesRecording.toArray(seriesRecordings)); + if (mDvrDataManager.isInitialized()) { + startEntranceTransition(); + } mDvrDataManager.removeDvrScheduleLoadFinishedListener(this); } @Override public void onRecordedProgramLoadFinished() { - startBrowseIfDvrInitialized(); + for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { + handleRecordedProgramAdded(recordedProgram, true); + } + updateRows(); + if (mDvrDataManager.isInitialized()) { + startEntranceTransition(); + } mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); } @@ -281,18 +270,6 @@ public class DvrBrowseFragment extends BrowseFragment implements } } - private void onScheduledRecordingConflictStatusChanged(ScheduledRecording... schedules) { - for (ScheduledRecording schedule : schedules) { - if (needToShowScheduledRecording(schedule)) { - if (mScheduleAdapter.contains(schedule)) { - mScheduleAdapter.change(schedule); - } - } else { - mScheduleAdapter.removeWithId(schedule); - } - } - } - @Override public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { handleSeriesRecordingsAdded(Arrays.asList(seriesRecordings)); @@ -318,53 +295,44 @@ public class DvrBrowseFragment extends BrowseFragment implements super.showTitle(flags); } - private void prepareUiElements() { + private void setupUiElements() { setBadgeDrawable(getActivity().getDrawable(R.drawable.ic_dvr_badge)); setHeadersState(HEADERS_ENABLED); setHeadersTransitionOnBackEnabled(false); setBrandColor(getResources().getColor(R.color.program_guide_side_panel_background, null)); - mRowsAdapter = new ArrayObjectAdapter(new DvrListRowPresenter(getContext())); - setAdapter(mRowsAdapter); - prepareEntranceTransition(); } - private boolean startBrowseIfDvrInitialized() { - if (mDvrDataManager.isInitialized()) { - // Setup rows - mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT); - mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT); - mSeriesAdapter = new SeriesAdapter(); - for (int i = 0; i < mGenreAdapters.length; i++) { - mGenreAdapters[i] = new RecordedProgramAdapter(); - } - // Schedule Recordings. - List<ScheduledRecording> schedules = mDvrDataManager.getAllScheduledRecordings(); - onScheduledRecordingAdded(ScheduledRecording.toArray(schedules)); - mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER); - // Recorded Programs. - for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - handleRecordedProgramAdded(recordedProgram, false); - } - // Series Recordings. Series recordings should be added after recorded programs, because - // we build series recordings' latest program information while adding recorded programs. - List<SeriesRecording> recordings = mDvrDataManager.getSeriesRecordings(); - handleSeriesRecordingsAdded(recordings); - mRecentRow = new ListRow(new HeaderItem( - getString(R.string.dvr_main_recent)), mRecentAdapter); - mRowsAdapter.add(new ListRow(new HeaderItem( - getString(R.string.dvr_main_scheduled)), mScheduleAdapter)); - mSeriesRow = new ListRow(new HeaderItem( - getString(R.string.dvr_main_series)), mSeriesAdapter); - updateRows(); - // Initialize listeners - mDvrDataManager.addRecordedProgramListener(this); - mDvrDataManager.addScheduledRecordingListener(this); - mDvrDataManager.addSeriesRecordingListener(this); - mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener); - startEntranceTransition(); - return true; - } - return false; + private void setupAdapters() { + mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT); + mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT); + mSeriesAdapter = new SeriesAdapter(); + for (int i = 0; i < mGenreAdapters.length; i++) { + mGenreAdapters[i] = new RecordedProgramAdapter(); + } + // Schedule Recordings. + List<ScheduledRecording> schedules = mDvrDataManager.getAllScheduledRecordings(); + onScheduledRecordingAdded(ScheduledRecording.toArray(schedules)); + mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER); + // Recorded Programs. + for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { + handleRecordedProgramAdded(recordedProgram, false); + } + // Series Recordings. Series recordings should be added after recorded programs, because + // we build series recordings' latest program information while adding recorded programs. + List<SeriesRecording> recordings = mDvrDataManager.getSeriesRecordings(); + handleSeriesRecordingsAdded(recordings); + mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); + mRecentRow = new ListRow(new HeaderItem( + getString(R.string.dvr_main_recent)), mRecentAdapter); + mRowsAdapter.add(new ListRow(new HeaderItem( + getString(R.string.dvr_main_scheduled)), mScheduleAdapter)); + mSeriesRow = new ListRow(new HeaderItem( + getString(R.string.dvr_main_series)), mSeriesAdapter); + updateRows(); + mDvrDataManager.addRecordedProgramListener(this); + mDvrDataManager.addScheduledRecordingListener(this); + mDvrDataManager.addSeriesRecordingListener(this); + setAdapter(mRowsAdapter); } private void handleRecordedProgramAdded(RecordedProgram recordedProgram, @@ -621,11 +589,10 @@ public class DvrBrowseFragment extends BrowseFragment implements @Override public long getId(Object item) { - // We takes the inverse number for the ID of recorded programs to make the ID stable. if (item instanceof SeriesRecording) { return ((SeriesRecording) item).getId(); } else if (item instanceof RecordedProgram) { - return -((RecordedProgram) item).getId() - 1; + return ((RecordedProgram) item).getId(); } else { return -1; } diff --git a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java index 880dc8ac..837d8ab2 100644 --- a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java +++ b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java @@ -27,7 +27,7 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelRecordConflictFragment; import java.util.ArrayList; diff --git a/src/com/android/tv/dvr/ui/DvrConflictFragment.java b/src/com/android/tv/dvr/ui/DvrConflictFragment.java index 5985f56f..e7be4d0a 100644 --- a/src/com/android/tv/dvr/ui/DvrConflictFragment.java +++ b/src/com/android/tv/dvr/ui/DvrConflictFragment.java @@ -34,9 +34,10 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.Program; -import com.android.tv.dvr.recorder.ConflictChecker; -import com.android.tv.dvr.recorder.ConflictChecker.OnUpcomingConflictChangeListener; -import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ConflictChecker; +import com.android.tv.dvr.ConflictChecker.OnUpcomingConflictChangeListener; +import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.util.Utils; import java.util.ArrayList; diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/DvrDetailsActivity.java index 30c81e83..806c775c 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java +++ b/src/com/android/tv/dvr/ui/DvrDetailsActivity.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; import android.app.Activity; import android.os.Bundle; diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/DvrDetailsFragment.java index 4d3698ef..21f9c4b4 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/DvrDetailsFragment.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; import android.content.Context; import android.content.Intent; @@ -48,8 +48,8 @@ import com.android.tv.data.BaseProgram; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.dialog.PinDialogFragment; -import com.android.tv.dvr.data.RecordedProgram; -import com.android.tv.dvr.ui.playback.DvrPlaybackActivity; +import com.android.tv.dvr.DvrPlaybackActivity; +import com.android.tv.dvr.RecordedProgram; import com.android.tv.parental.ParentalControlSettings; import com.android.tv.util.ImageLoader; import com.android.tv.util.ToastUtils; diff --git a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java new file mode 100644 index 00000000..73ddcdd0 --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java @@ -0,0 +1,87 @@ +/* + * 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.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v17.leanback.widget.GuidanceStylist.Guidance; +import android.support.v17.leanback.widget.GuidedAction; +import android.text.TextUtils; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; + +import java.util.List; + +public class DvrForgetStorageErrorFragment extends DvrGuidedStepFragment { + private static final int ACTION_CANCEL = 1; + private static final int ACTION_FORGET_STORAGE = 2; + private String mInputId; + + @Override + public void onCreate(Bundle savedInstanceState) { + Bundle args = getArguments(); + if (args != null) { + mInputId = args.getString(DvrHalfSizedDialogFragment.KEY_INPUT_ID); + } + SoftPreconditions.checkArgument(!TextUtils.isEmpty(mInputId)); + super.onCreate(savedInstanceState); + } + + @NonNull + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getResources().getString(R.string.dvr_error_forget_storage_title); + String description = getResources().getString( + R.string.dvr_error_forget_storage_description); + return new Guidance(title, description, null, null); + } + + @Override + public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { + Activity activity = getActivity(); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_CANCEL) + .title(getResources().getString(R.string.dvr_action_error_cancel)) + .build()); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_FORGET_STORAGE) + .title(getResources().getString(R.string.dvr_action_error_forget_storage)) + .build()); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + if (action.getId() != ACTION_FORGET_STORAGE) { + dismissDialog(); + return; + } + DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); + dvrManager.forgetStorage(mInputId); + Activity activity = getActivity(); + if (activity instanceof DvrDetailsActivity) { + // Since we removed everything, just finish the activity. + activity.finish(); + } else { + dismissDialog(); + } + } +} diff --git a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java index 433588da..d26e6836 100644 --- a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java +++ b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java @@ -16,12 +16,10 @@ package com.android.tv.dvr.ui; -import android.app.Activity; import android.app.DialogFragment; 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.GuidedAction; import android.support.v17.leanback.widget.VerticalGridView; import android.view.LayoutInflater; @@ -31,26 +29,11 @@ import android.view.ViewGroup; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dialog.HalfSizedDialogFragment.OnActionClickListener; import com.android.tv.dialog.SafeDismissDialogFragment; import com.android.tv.dvr.DvrManager; - -import java.util.List; +import com.android.tv.dvr.ui.HalfSizedDialogFragment.OnActionClickListener; public class DvrGuidedStepFragment extends GuidedStepFragment { - /** - * Action ID for "recording/scheduling the program anyway". - */ - public static final int ACTION_RECORD_ANYWAY = 1; - /** - * Action ID for "deleting existed recordings". - */ - public static final int ACTION_DELETE_RECORDINGS = 2; - /** - * Action ID for "cancelling current recording request". - */ - public static final int ACTION_CANCEL_RECORDING = 3; - private DvrManager mDvrManager; private OnActionClickListener mOnActionClickListener; @@ -103,35 +86,4 @@ public class DvrGuidedStepFragment extends GuidedStepFragment { protected void setOnActionClickListener(OnActionClickListener listener) { mOnActionClickListener = listener; } - - /** - * The inner guided step fragment for - * {@link com.android.tv.dvr.ui.DvrHalfSizedDialogFragment - * .DvrNoFreeSpaceErrorDialogFragment}. - */ - public static class DvrNoFreeSpaceErrorFragment - extends DvrGuidedStepFragment { - @Override - public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { - return new GuidanceStylist.Guidance(getString(R.string.dvr_error_no_free_space_title), - getString(R.string.dvr_error_no_free_space_description), null, null); - } - - @Override - public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { - Activity activity = getActivity(); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_RECORD_ANYWAY) - .title(R.string.dvr_action_record_anyway) - .build()); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_DELETE_RECORDINGS) - .title(R.string.dvr_action_delete_recordings) - .build()); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_CANCEL_RECORDING) - .title(R.string.dvr_action_record_cancel) - .build()); - } - } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java index 9054dd03..2b132db8 100644 --- a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java +++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java @@ -29,7 +29,6 @@ import android.view.ViewGroup; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.dvr.DvrStorageStatusManager; -import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelWatchConflictFragment; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; import com.android.tv.guide.ProgramGuide; @@ -167,17 +166,6 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { } /** - * A dialog fragment to show error message when there is no enough free space to record. - */ - public static class DvrNoFreeSpaceErrorDialogFragment - extends DvrGuidedStepDialogFragment { - @Override - protected DvrGuidedStepFragment onCreateGuidedStepFragment() { - return new DvrGuidedStepFragment.DvrNoFreeSpaceErrorFragment(); - } - } - - /** * A dialog fragment to show error message when the current storage is too small to * support DVR */ diff --git a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java index 3c5df1a6..3b1dbfa0 100644 --- a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java +++ b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java @@ -17,7 +17,6 @@ package com.android.tv.dvr.ui; import android.app.Activity; -import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; @@ -25,67 +24,19 @@ import android.support.v17.leanback.widget.GuidedAction; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.ui.browse.DvrBrowseActivity; +import com.android.tv.dvr.DvrDataManager; -import java.util.ArrayList; import java.util.List; public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment { - /** - * Key for the failed scheduled recordings information. - */ - public static final String FAILED_SCHEDULED_RECORDING_INFOS = - "failed_scheduled_recording_infos"; - - private static final String TAG = "DvrInsufficientSpaceErrorFragment"; - - private static final int ACTION_VIEW_RECENT_RECORDINGS = 1; - - private ArrayList<String> mFailedScheduledRecordingInfos; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - Bundle args = getArguments(); - if (args != null) { - mFailedScheduledRecordingInfos = - args.getStringArrayList(FAILED_SCHEDULED_RECORDING_INFOS); - } - SoftPreconditions.checkState( - mFailedScheduledRecordingInfos != null && !mFailedScheduledRecordingInfos.isEmpty(), - TAG, "failed scheduled recording is null"); - } + private static final int ACTION_DONE = 1; + private static final int ACTION_OPEN_DVR = 2; @Override public Guidance onCreateGuidance(Bundle savedInstanceState) { - String title; - String description; - int failedScheduledRecordingSize = mFailedScheduledRecordingInfos.size(); - if (failedScheduledRecordingSize == 1) { - title = getString( - R.string.dvr_error_insufficient_space_title_one_recording, - mFailedScheduledRecordingInfos.get(0)); - description = getString( - R.string.dvr_error_insufficient_space_description_one_recording, - mFailedScheduledRecordingInfos.get(0)); - } else if (failedScheduledRecordingSize == 2) { - title = getString( - R.string.dvr_error_insufficient_space_title_two_recordings, - mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1)); - description = getString( - R.string.dvr_error_insufficient_space_description_two_recordings, - mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1)); - } else { - title = getString( - R.string.dvr_error_insufficient_space_title_three_or_more_recordings, - mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1), - mFailedScheduledRecordingInfos.get(2)); - description = getString( - R.string.dvr_error_insufficient_space_description_three_or_more_recordings, - mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1), - mFailedScheduledRecordingInfos.get(2)); - } + String title = getResources().getString(R.string.dvr_error_insufficient_space_title); + String description = getResources() + .getString(R.string.dvr_error_insufficient_space_description); return new Guidance(title, description, null, null); } @@ -93,21 +44,26 @@ public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment { public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { Activity activity = getActivity(); actions.add(new GuidedAction.Builder(activity) - .clickAction(GuidedAction.ACTION_ID_OK) + .id(ACTION_DONE) + .title(getResources().getString(R.string.dvr_action_error_done)) .build()); - if (TvApplication.getSingletons(getContext()).getDvrManager().hasValidItems()) { - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_VIEW_RECENT_RECORDINGS) - .title(getResources().getString( - R.string.dvr_error_insufficient_space_action_view_recent_recordings)) - .build()); + DvrDataManager dvrDataManager = TvApplication.getSingletons(getContext()) + .getDvrDataManager(); + if (!(dvrDataManager.getRecordedPrograms().isEmpty() + && dvrDataManager.getStartedRecordings().isEmpty() + && dvrDataManager.getNonStartedScheduledRecordings().isEmpty() + && dvrDataManager.getSeriesRecordings().isEmpty())) { + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_OPEN_DVR) + .title(getResources().getString(R.string.dvr_action_error_open_dvr)) + .build()); } } @Override public void onGuidedActionClicked(GuidedAction action) { - if (action.getId() == ACTION_VIEW_RECENT_RECORDINGS) { - Intent intent = new Intent(getActivity(), DvrBrowseActivity.class); + if (action.getId() == ACTION_OPEN_DVR) { + Intent intent = new Intent(getActivity(), DvrActivity.class); getActivity().startActivity(intent); } dismissDialog(); diff --git a/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java b/src/com/android/tv/dvr/ui/DvrItemPresenter.java index 317b6af3..339e5d2f 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java +++ b/src/com/android/tv/dvr/ui/DvrItemPresenter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; import android.app.Activity; import android.support.annotation.CallSuper; @@ -22,17 +22,16 @@ import android.support.v17.leanback.widget.Presenter; import android.view.View; import android.view.View.OnClickListener; -import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.dvr.DvrUiHelper; import java.util.HashSet; +import java.util.Iterator; import java.util.Set; /** * An abstract class to present DVR items in {@link RecordingCardView}, which is mainly used in - * {@link DvrBrowseFragment}. DVR items might include: - * {@link com.android.tv.dvr.data.ScheduledRecording}, - * {@link com.android.tv.dvr.data.RecordedProgram}, and - * {@link com.android.tv.dvr.data.SeriesRecording}. + * {@link DvrBrowseFragment}. DVR items might include: {@link ScheduledRecording}, + * {@link RecordedProgram}, and {@link SeriesRecording}. */ public abstract class DvrItemPresenter extends Presenter { private final Set<ViewHolder> mBoundViewHolders = new HashSet<>(); @@ -50,8 +49,6 @@ public abstract class DvrItemPresenter extends Presenter { @CallSuper public void onUnbindViewHolder(ViewHolder viewHolder) { mBoundViewHolders.remove(viewHolder); - viewHolder.view.setTag(null); - viewHolder.view.setOnClickListener(null); } /** diff --git a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java index 8dc9eb4e..2e2c2849 100644 --- a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java +++ b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java @@ -17,27 +17,29 @@ package com.android.tv.dvr.ui; import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.Intent; import android.os.Bundle; -import android.provider.Settings; +import android.support.v17.leanback.app.GuidedStepFragment; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; -import android.util.Log; +import android.text.TextUtils; import com.android.tv.R; -import com.android.tv.dvr.ui.browse.DvrDetailsActivity; +import com.android.tv.common.SoftPreconditions; import java.util.List; public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment { - private static String TAG = "DvrMissingStorageErrorFragment"; - - private static final int ACTION_OK = 1; - private static final int ACTION_OPEN_STORAGE_SETTINGS = 2; + private static final int ACTION_CANCEL = 1; + private static final int ACTION_FORGET_STORAGE = 2; + private String mInputId; @Override public void onCreate(Bundle savedInstanceState) { + Bundle args = getArguments(); + if (args != null) { + mInputId = args.getString(DvrHalfSizedDialogFragment.KEY_INPUT_ID); + } + SoftPreconditions.checkArgument(!TextUtils.isEmpty(mInputId)); super.onCreate(savedInstanceState); } @@ -53,31 +55,25 @@ public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment { public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { Activity activity = getActivity(); actions.add(new GuidedAction.Builder(activity) - .id(ACTION_OK) - .title(android.R.string.ok) + .id(ACTION_CANCEL) + .title(getResources().getString(R.string.dvr_action_error_cancel)) .build()); actions.add(new GuidedAction.Builder(activity) - .id(ACTION_OPEN_STORAGE_SETTINGS) - .title(getResources().getString(R.string.dvr_action_error_storage_settings)) + .id(ACTION_FORGET_STORAGE) + .title(getResources().getString(R.string.dvr_action_error_forget_storage)) .build()); } @Override public void onGuidedActionClicked(GuidedAction action) { - Activity activity = getActivity(); - if (activity instanceof DvrDetailsActivity) { - activity.finish(); - } else { - dismissDialog(); - } - if (action.getId() != ACTION_OPEN_STORAGE_SETTINGS) { + if (action.getId() == ACTION_FORGET_STORAGE) { + DvrForgetStorageErrorFragment fragment = new DvrForgetStorageErrorFragment(); + Bundle args = new Bundle(); + args.putString(DvrHalfSizedDialogFragment.KEY_INPUT_ID, mInputId); + fragment.setArguments(args); + GuidedStepFragment.add(getFragmentManager(), fragment, R.id.halfsized_dialog_host); return; } - final Intent intent = new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS); - try { - getContext().startActivity(intent); - } catch (ActivityNotFoundException e) { - Log.e(TAG, "Can't start internal storage settings activity", e); - } + dismissDialog(); } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java b/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java index 4bd121b1..8c4c856c 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java +++ b/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.android.tv.dvr.ui.playback; +package com.android.tv.dvr.ui; import android.content.Context; import android.content.Intent; +import android.content.res.Resources; import android.text.TextUtils; import android.util.Log; import android.view.View; @@ -25,25 +26,22 @@ import android.view.View.OnClickListener; import android.view.ViewGroup; import com.android.tv.R; -import com.android.tv.dvr.data.RecordedProgram; -import com.android.tv.dvr.ui.browse.RecordedProgramPresenter; -import com.android.tv.dvr.ui.browse.RecordingCardView; +import com.android.tv.dvr.RecordedProgram; +import com.android.tv.dvr.DvrPlaybackActivity; import com.android.tv.util.Utils; /** * This class is used to generate Views and bind Objects for related recordings in DVR playback. */ -class DvrPlaybackCardPresenter extends RecordedProgramPresenter { +public class DvrPlaybackCardPresenter extends RecordedProgramPresenter { private static final String TAG = "DvrPlaybackCardPresenter"; private static final boolean DEBUG = false; private final int mRelatedRecordingCardWidth; private final int mRelatedRecordingCardHeight; - private final DvrPlaybackOverlayFragment mFragment; - DvrPlaybackCardPresenter(Context context, DvrPlaybackOverlayFragment fragment) { + DvrPlaybackCardPresenter(Context context) { super(context); - mFragment = fragment; mRelatedRecordingCardWidth = context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_width); mRelatedRecordingCardHeight = @@ -52,8 +50,9 @@ class DvrPlaybackCardPresenter extends RecordedProgramPresenter { @Override public ViewHolder onCreateViewHolder(ViewGroup parent) { + Resources res = parent.getResources(); RecordingCardView view = new RecordingCardView( - getContext(), mRelatedRecordingCardWidth, mRelatedRecordingCardHeight, true); + getContext(), mRelatedRecordingCardWidth, mRelatedRecordingCardHeight); return new ViewHolder(view); } @@ -62,10 +61,6 @@ class DvrPlaybackCardPresenter extends RecordedProgramPresenter { return new OnClickListener() { @Override public void onClick(View v) { - // Disable fading of overlay fragment to prevent the layout blinking while updating - // new playback states and info. The fading enabled status will be reset during - // playback state changing, in DvrPlaybackControlHelper.onStateChanged(). - mFragment.setFadingEnabled(false); long programId = ((RecordedProgram) v.getTag()).getId(); if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId); Intent intent = new Intent(getContext(), DvrPlaybackActivity.class); @@ -74,4 +69,14 @@ class DvrPlaybackCardPresenter extends RecordedProgramPresenter { } }; } + + @Override + protected String getDescription(RecordedProgram program) { + String description = program.getDescription(); + if (TextUtils.isEmpty(description)) { + description = + getContext().getResources().getString(R.string.dvr_msg_no_program_description); + } + return description; + } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java index 4658a328..0bc4ecb1 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java +++ b/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java @@ -14,26 +14,19 @@ * limitations under the License */ -package com.android.tv.dvr.ui.playback; +package com.android.tv.dvr.ui; import android.app.Activity; -import android.content.Context; import android.graphics.drawable.Drawable; import android.media.MediaMetadata; import android.media.session.MediaController; import android.media.session.MediaController.TransportControls; import android.media.session.PlaybackState; -import android.media.tv.TvTrackInfo; -import android.os.Bundle; import android.support.v17.leanback.app.PlaybackControlGlue; import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter; import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.ArrayObjectAdapter; -import android.support.v17.leanback.widget.ControlButtonPresenterSelector; import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.PlaybackControlsRow; -import android.support.v17.leanback.widget.PlaybackControlsRow.ClosedCaptioningAction; -import android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction; import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; import android.support.v17.leanback.widget.RowPresenter; import android.text.TextUtils; @@ -44,18 +37,19 @@ import android.view.View; import com.android.tv.R; import com.android.tv.util.TimeShiftUtils; -import java.util.ArrayList; - /** * A helper class to assist {@link DvrPlaybackOverlayFragment} to manage its controls row and * send command to the media controller. It also helps to update playback states displayed in the * fragment according to information the media session provides. */ -class DvrPlaybackControlHelper extends PlaybackControlGlue { +public class DvrPlaybackControlHelper extends PlaybackControlGlue { private static final String TAG = "DvrPlaybackControlHelper"; private static final boolean DEBUG = false; - private static final int AUDIO_ACTION_ID = 1001; + /** + * Indicates the ID of the media under playback is unknown. + */ + public static int UNKNOWN_MEDIA_ID = -1; private int mPlaybackState = PlaybackState.STATE_NONE; private int mPlaybackSpeedLevel; @@ -66,9 +60,6 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback(); private final TransportControls mTransportControls; private final int mExtraPaddingTopForNoDescription; - private final ArrayObjectAdapter mSecondaryActionsAdapter; - private final MultiAction mClosedCaptioningAction; - private final MultiAction mMultiAudioAction; public DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) { super(activity, overlayFragment, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]); @@ -77,15 +68,11 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { mTransportControls = mMediaController.getTransportControls(); mExtraPaddingTopForNoDescription = activity.getResources() .getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top); - mSecondaryActionsAdapter = new ArrayObjectAdapter(new ControlButtonPresenterSelector()); - mClosedCaptioningAction = new ClosedCaptioningAction(activity); - mMultiAudioAction = new MultiAudioAction(activity); } @Override public PlaybackControlsRowPresenter createControlsRowAndPresenter() { PlaybackControlsRow controlsRow = new PlaybackControlsRow(this); - controlsRow.setSecondaryActionsAdapter(mSecondaryActionsAdapter); setControlsRow(controlsRow); AbstractDetailsDescriptionPresenter detailsPresenter = new AbstractDetailsDescriptionPresenter() { @@ -129,21 +116,7 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { @Override public void onActionClicked(Action action) { if (mReadyToControl) { - int trackType; - if (action.getId() == mClosedCaptioningAction.getId()) { - trackType = TvTrackInfo.TYPE_SUBTITLE; - } else if (action.getId() == AUDIO_ACTION_ID) { - trackType = TvTrackInfo.TYPE_AUDIO; - } else { - DvrPlaybackControlHelper.super.onActionClicked(action); - return; - } - ArrayList<TvTrackInfo> trackInfos = - ((DvrPlaybackOverlayFragment) getFragment()).getTracks(trackType); - if (!trackInfos.isEmpty()) { - showSideFragment(trackInfos, ((DvrPlaybackOverlayFragment) - getFragment()).getSelectedTrackId(trackType)); - } + DvrPlaybackControlHelper.super.onActionClicked(action); } } }); @@ -185,10 +158,10 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { /** * Returns the ID of the media under playback. */ - public String getMediaId() { + public long getMediaId() { MediaMetadata mediaMetadata = mMediaController.getMetadata(); - return mediaMetadata == null ? null - : mediaMetadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); + return mediaMetadata == null ? UNKNOWN_MEDIA_ID + : mediaMetadata.getLong(MediaMetadata.METADATA_KEY_MEDIA_ID); } @Override @@ -244,37 +217,6 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { mMediaController.unregisterCallback(mMediaControllerCallback); } - /** - * Update the secondary controls row. - * @param hasClosedCaption {@code true} to show the closed caption selection button, - * {@code false} to hide it. - * @param hasMultiAudio {@code true} to show the audio track selection button, - * {@code false} to hide it. - */ - public void updateSecondaryRow(boolean hasClosedCaption, boolean hasMultiAudio) { - if (hasClosedCaption) { - if (mSecondaryActionsAdapter.indexOf(mClosedCaptioningAction) < 0) { - mSecondaryActionsAdapter.add(0, mClosedCaptioningAction); - } - } else { - mSecondaryActionsAdapter.remove(mClosedCaptioningAction); - } - if (hasMultiAudio) { - if (mSecondaryActionsAdapter.indexOf(mMultiAudioAction) < 0) { - mSecondaryActionsAdapter.add(mMultiAudioAction); - } - } else { - mSecondaryActionsAdapter.remove(mMultiAudioAction); - } - } - - /** - * Returns if the secondary controls row has any buttons and thus should be shown. - */ - public boolean hasSecondaryRow() { - return mSecondaryActionsAdapter.size() != 0; - } - @Override protected void startPlayback(int speedId) { if (getCurrentSpeedId() == speedId) { @@ -309,14 +251,6 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { // Do nothing. } - /** - * Notifies closed caption being enabled/disabled to update related UI. - */ - void onSubtitleTrackStateChanged(boolean enabled) { - mClosedCaptioningAction.setIndex(enabled ? - ClosedCaptioningAction.ON : ClosedCaptioningAction.OFF); - } - private void onStateChanged(int state, long positionMs, int speedLevel) { if (DEBUG) Log.d(TAG, "onStateChanged"); getControlsRow().setCurrentTime((int) positionMs); @@ -363,19 +297,6 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { onStateChanged(); } - private void showSideFragment(ArrayList<TvTrackInfo> trackInfos, String selectedTrackId) { - Bundle args = new Bundle(); - args.putParcelableArrayList(DvrPlaybackSideFragment.TRACK_INFOS, trackInfos); - args.putString(DvrPlaybackSideFragment.SELECTED_TRACK_ID, selectedTrackId); - DvrPlaybackSideFragment sideFragment = new DvrPlaybackSideFragment(); - sideFragment.setArguments(args); - getFragment().getFragmentManager().beginTransaction() - .hide(getFragment()) - .replace(R.id.dvr_playback_side_fragment, sideFragment) - .addToBackStack(null) - .commit(); - } - private class MediaControllerCallback extends MediaController.Callback { @Override public void onPlaybackStateChanged(PlaybackState state) { @@ -389,11 +310,4 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { ((DvrPlaybackOverlayFragment) getFragment()).onMediaControllerUpdated(); } } - - private static class MultiAudioAction extends MultiAction { - MultiAudioAction(Context context) { - super(AUDIO_ACTION_ID); - setDrawables(new Drawable[]{context.getDrawable(R.drawable.ic_tvoption_multi_track)}); - } - } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java index ff907182..51ec93b8 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java +++ b/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java @@ -14,14 +14,13 @@ * limitations under the License */ -package com.android.tv.dvr.ui.playback; +package com.android.tv.dvr.ui; import android.content.Context; import android.content.Intent; import android.graphics.Point; import android.hardware.display.DisplayManager; import android.media.tv.TvContentRating; -import android.media.tv.TvTrackInfo; import android.os.Bundle; import android.media.session.PlaybackState; import android.media.tv.TvInputManager; @@ -31,6 +30,7 @@ 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.PlaybackControlsRow; import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; import android.support.v17.leanback.widget.SinglePresenterSelector; @@ -38,25 +38,20 @@ import android.view.Display; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; +import android.text.TextUtils; import android.util.Log; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.BaseProgram; +import com.android.tv.dvr.RecordedProgram; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.data.RecordedProgram; -import com.android.tv.dvr.data.SeriesRecording; -import com.android.tv.dvr.ui.SortedArrayAdapter; -import com.android.tv.dvr.ui.browse.DvrListRowPresenter; +import com.android.tv.dvr.DvrPlayer; +import com.android.tv.dvr.DvrPlaybackMediaSessionHelper; import com.android.tv.parental.ContentRatingsManager; -import com.android.tv.util.TvSettings; -import com.android.tv.util.TvTrackInfoUtils; import com.android.tv.util.Utils; -import java.util.List; -import java.util.ArrayList; - public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { // TODO: Handles audio focus. Deals with block and ratings. private static final String TAG = "DvrPlaybackOverlayFragment"; @@ -67,7 +62,6 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { // mProgram is only used to store program from intent. Don't use it elsewhere. private RecordedProgram mProgram; - private DvrPlayer mDvrPlayer; private DvrPlaybackMediaSessionHelper mMediaSessionHelper; private DvrPlaybackControlHelper mPlaybackControlHelper; private ArrayObjectAdapter mRowsAdapter; @@ -78,30 +72,19 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { private TvView mTvView; private View mBlockScreenView; private ListRow mRelatedRecordingsRow; - private int mPaddingWithoutRelatedRow; - private int mPaddingWithoutSecondaryRow; + private int mExtraPaddingNoRelatedRow; private int mWindowWidth; private int mWindowHeight; private float mAppliedAspectRatio; private float mWindowAspectRatio; private boolean mPinChecked; - private DvrPlayer.OnTrackSelectedListener mOnSubtitleTrackSelectedListener = - new DvrPlayer.OnTrackSelectedListener() { - @Override - public void onTrackSelected(String selectedTrackId) { - mPlaybackControlHelper.onSubtitleTrackStateChanged(selectedTrackId != null); - mRowsAdapter.notifyArrayItemRangeChanged(0, 1); - } - }; @Override public void onCreate(Bundle savedInstanceState) { if (DEBUG) Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); - mPaddingWithoutRelatedRow = getActivity().getResources() - .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_no_related_row); - mPaddingWithoutSecondaryRow = getActivity().getResources() - .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_no_secondary_row); + mExtraPaddingNoRelatedRow = getActivity().getResources() + .getDimensionPixelOffset(R.dimen.dvr_playback_fragment_extra_padding_top); mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager(); mContentRatingsManager = TvApplication.getSingletons(getContext()) .getTvInputManagerHelper().getContentRatingsManager(); @@ -127,31 +110,13 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { super.onActivityCreated(savedInstanceState); mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view); mBlockScreenView = getActivity().findViewById(R.id.block_screen); - mDvrPlayer = new DvrPlayer(mTvView); mMediaSessionHelper = new DvrPlaybackMediaSessionHelper( - getActivity(), MEDIA_SESSION_TAG, mDvrPlayer, this); + getActivity(), MEDIA_SESSION_TAG, new DvrPlayer(mTvView), this); mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this); setUpRows(); - mDvrPlayer.setOnTracksAvailabilityChangedListener( - new DvrPlayer.OnTracksAvailabilityChangedListener() { - @Override - public void onTracksAvailabilityChanged(boolean hasClosedCaption, - boolean hasMultiAudio) { - mPlaybackControlHelper.updateSecondaryRow(hasClosedCaption, hasMultiAudio); - if (hasClosedCaption) { - mDvrPlayer.setOnTrackSelectedListener(TvTrackInfo.TYPE_SUBTITLE, - mOnSubtitleTrackSelectedListener); - selectBestMatchedTrack(TvTrackInfo.TYPE_SUBTITLE); - } else { - mDvrPlayer.setOnTrackSelectedListener(TvTrackInfo.TYPE_SUBTITLE, null); - } - if (hasMultiAudio) { - selectBestMatchedTrack(TvTrackInfo.TYPE_AUDIO); - } - onMediaControllerUpdated(); - } - }); - mDvrPlayer.setOnAspectRatioChangedListener(new DvrPlayer.OnAspectRatioChangedListener() { + preparePlayback(getActivity().getIntent()); + DvrPlayer dvrPlayer = mMediaSessionHelper.getDvrPlayer(); + dvrPlayer.setAspectRatioChangedListener(new DvrPlayer.AspectRatioChangedListener() { @Override public void onAspectRatioChanged(float videoAspectRatio) { updateAspectRatio(videoAspectRatio); @@ -159,7 +124,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { }); mPinChecked = getActivity().getIntent() .getBooleanExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, false); - mDvrPlayer.setOnContentBlockedListener(new DvrPlayer.OnContentBlockedListener() { + dvrPlayer.setContentBlockedListener(new DvrPlayer.ContentBlockedListener() { @Override public void onContentBlocked(TvContentRating rating) { if (mPinChecked) { @@ -184,7 +149,6 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { .show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); } }); - preparePlayback(getActivity().getIntent()); } @Override @@ -236,9 +200,6 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { updateAspectRatio(mAppliedAspectRatio); } - /** - * Returns next recorded episode in the same series as now playing program. - */ public RecordedProgram getNextEpisode(RecordedProgram program) { int position = mRelatedRecordingsRowAdapter.findInsertPosition(program); if (position == mRelatedRecordingsRowAdapter.size()) { @@ -248,92 +209,16 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { } } - /** - * Returns the tracks of the give type of the current playback. - - * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} - * or {@link TvTrackInfo#TYPE_AUDIO}. Or returns {@code null}. - */ - public ArrayList<TvTrackInfo> getTracks(int trackType) { - if (trackType == TvTrackInfo.TYPE_AUDIO) { - return mDvrPlayer.getAudioTracks(); - } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { - return mDvrPlayer.getSubtitleTracks(); - } - return null; - } - - /** - * Returns the ID of the selected track of the given type. - */ - public String getSelectedTrackId(int trackType) { - return mDvrPlayer.getSelectedTrackId(trackType); - } - - /** - * Returns the language setting of the given track type. - - * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} - * or {@link TvTrackInfo#TYPE_AUDIO}. - * @return {@code null} if no language has been set for the given track type. - */ - TvTrackInfo getTrackSetting(int trackType) { - return TvSettings.getDvrPlaybackTrackSettings(getContext(), trackType); - } - - /** - * Selects the given audio or subtitle track for DVR playback. - * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} - * or {@link TvTrackInfo#TYPE_AUDIO}. - * @param selectedTrack {@code null} to disable the audio or subtitle track according to - * trackType. - */ - void selectTrack(int trackType, TvTrackInfo selectedTrack) { - if (mDvrPlayer.isPlaybackPrepared()) { - mDvrPlayer.selectTrack(trackType, selectedTrack); - } - } - - /** - * Notifies the content of controls row or related recordings row is changed and the UI should - * be updated according to the change. - */ void onMediaControllerUpdated() { - updateVerticalPosition(); - mRowsAdapter.notifyArrayItemRangeChanged(0, 2); - } - - private void selectBestMatchedTrack(int trackType) { - TvTrackInfo selectedTrack = getTrackSetting(trackType); - if (selectedTrack != null) { - TvTrackInfo bestMatchedTrack = TvTrackInfoUtils.getBestTrackInfo(getTracks(trackType), - selectedTrack.getId(), selectedTrack.getLanguage(), - trackType == TvTrackInfo.TYPE_AUDIO ? selectedTrack.getAudioChannelCount() : 0); - if (bestMatchedTrack != null && (trackType == TvTrackInfo.TYPE_AUDIO || Utils - .isEqualLanguage(bestMatchedTrack.getLanguage(), - selectedTrack.getLanguage()))) { - selectTrack(trackType, bestMatchedTrack); - return; - } - } - if (trackType == TvTrackInfo.TYPE_SUBTITLE) { - // Disables closed captioning if there's no matched language. - selectTrack(TvTrackInfo.TYPE_SUBTITLE, null); - } + mRowsAdapter.notifyArrayItemRangeChanged(0, 1); } private void updateAspectRatio(float videoAspectRatio) { - if (videoAspectRatio <= 0) { - // We don't have video's width or height information, use window's aspect ratio. - videoAspectRatio = mWindowAspectRatio; - } if (Math.abs(mAppliedAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) { // No need to change return; } - if (Math.abs(mWindowAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) { - ((ViewGroup) mTvView.getParent()).setPadding(0, 0, 0, 0); - } else if (videoAspectRatio < mWindowAspectRatio) { + if (videoAspectRatio < mWindowAspectRatio) { int newPadding = (mWindowWidth - Math.round(mWindowHeight * videoAspectRatio)) / 2; ((ViewGroup) mTvView.getParent()).setPadding(newPadding, 0, newPadding, 0); } else { @@ -345,7 +230,6 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { private void preparePlayback(Intent intent) { mMediaSessionHelper.setupPlayback(mProgram, getSeekTimeFromIntent(intent)); - mPlaybackControlHelper.updateSecondaryRow(false, false); getActivity().getMediaController().getTransportControls().prepare(); updateRelatedRecordingsRow(); } @@ -355,35 +239,24 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { mRelatedRecordingsRowAdapter.clear(); long programId = mProgram.getId(); String seriesId = mProgram.getSeriesId(); - SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); - if (seriesRecording != null) { + if (!TextUtils.isEmpty(seriesId)) { if (DEBUG) Log.d(TAG, "Update related recordings with:" + seriesId); - List<RecordedProgram> relatedPrograms = - mDvrDataManager.getRecordedPrograms(seriesRecording.getId()); - for (RecordedProgram program : relatedPrograms) { - if (programId != program.getId()) { + for (RecordedProgram program : mDvrDataManager.getRecordedPrograms()) { + if (seriesId.equals(program.getSeriesId()) && programId != program.getId()) { mRelatedRecordingsRowAdapter.add(program); } } } + View view = getView(); if (mRelatedRecordingsRowAdapter.size() == 0) { mRowsAdapter.remove(mRelatedRecordingsRow); + view.setPadding(view.getPaddingLeft(), mExtraPaddingNoRelatedRow, + view.getPaddingRight(), view.getPaddingBottom()); } else if (wasEmpty){ mRowsAdapter.add(mRelatedRecordingsRow); + view.setPadding(view.getPaddingLeft(), 0, + view.getPaddingRight(), view.getPaddingBottom()); } - onMediaControllerUpdated(); - } - - private void updateVerticalPosition() { - int verticalPadding = 0; - verticalPadding += - mRelatedRecordingsRowAdapter.size() == 0 ? mPaddingWithoutRelatedRow : 0; - verticalPadding += - mPlaybackControlHelper.hasSecondaryRow() ? 0 : mPaddingWithoutSecondaryRow; - if (DEBUG) Log.d(TAG, "New controls padding: " + verticalPadding); - View view = getView(); - view.setPadding(view.getPaddingLeft(), verticalPadding, - view.getPaddingRight(), view.getPaddingBottom()); } private void setUpRows() { @@ -392,7 +265,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { ClassPresenterSelector selector = new ClassPresenterSelector(); selector.addClassPresenter(PlaybackControlsRow.class, controlsRowPresenter); - selector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext())); + selector.addClassPresenter(ListRow.class, new ListRowPresenter()); mRowsAdapter = new ArrayObjectAdapter(selector); mRowsAdapter.add(mPlaybackControlHelper.getControlsRow()); @@ -401,7 +274,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { } private ListRow getRelatedRecordingsRow() { - mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity(), this); + mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity()); mRelatedRecordingsRowAdapter = new RelatedRecordingsAdapter(mRelatedRecordingCardPresenter); HeaderItem header = new HeaderItem(0, getActivity().getString(R.string.dvr_playback_related_recordings)); @@ -424,7 +297,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { } @Override - public long getId(BaseProgram item) { + long getId(BaseProgram item) { return item.getId(); } } diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java index d6008315..da6d1637 100644 --- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java +++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java @@ -32,8 +32,9 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.SeriesRecording; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; import com.android.tv.util.Utils; @@ -47,26 +48,18 @@ import java.util.List; */ @TargetApi(Build.VERSION_CODES.N) public class DvrScheduleFragment extends DvrGuidedStepFragment { - /** - * Key for the whether to add the current program to series. - * Type: boolean - */ - public static final String KEY_ADD_CURRENT_PROGRAM_TO_SERIES = "add_current_program_to_series"; - private static final String TAG = "DvrScheduleFragment"; private static final int ACTION_RECORD_EPISODE = 1; private static final int ACTION_RECORD_SERIES = 2; private Program mProgram; - private boolean mAddCurrentProgramToSeries; @Override public void onCreate(Bundle savedInstanceState) { Bundle args = getArguments(); if (args != null) { mProgram = args.getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM); - mAddCurrentProgramToSeries = args.getBoolean(KEY_ADD_CURRENT_PROGRAM_TO_SERIES, false); } DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); SoftPreconditions.checkArgument(mProgram != null && mProgram.isEpisodic(), TAG, @@ -146,10 +139,8 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment { .build(); getDvrManager().updateSeriesRecording(seriesRecording); } - DvrUiHelper.startSeriesSettingsActivity(getContext(), - seriesRecording.getId(), null, true, true, true, - mAddCurrentProgramToSeries ? mProgram : null); + seriesRecording.getId(), null, true, true, true); dismissDialog(); } } diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java b/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java index a0410bb3..f6e6ac26 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui.list; +package com.android.tv.dvr.ui; import android.app.Activity; import android.app.ProgressDialog; @@ -24,13 +24,15 @@ import android.support.annotation.IntDef; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Program; -import com.android.tv.dvr.data.SeriesRecording; -import com.android.tv.dvr.provider.EpisodicProgramLoadTask; -import com.android.tv.dvr.recorder.SeriesRecordingScheduler; -import com.android.tv.dvr.ui.BigArguments; +import com.android.tv.dvr.EpisodicProgramLoadTask; +import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.SeriesRecordingScheduler; +import com.android.tv.dvr.ui.list.DvrSchedulesFragment; +import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -70,47 +72,33 @@ public class DvrSchedulesActivity extends Activity { getFragmentManager().beginTransaction().add( R.id.fragment_container, schedulesFragment).commit(); } else if (scheduleType == TYPE_SERIES_SCHEDULE) { - if (BigArguments.getArgument(DvrSeriesSchedulesFragment - .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS) != null) { - // The programs will be passed to the DvrSeriesSchedulesFragment, so don't need - // to reset the BigArguments. - showDvrSeriesSchedulesFragment(getIntent().getExtras()); - } else { - final ProgressDialog dialog = ProgressDialog.show(this, null, getString( - R.string.dvr_series_progress_message_reading_programs)); - SeriesRecording seriesRecording = getIntent().getExtras() - .getParcelable(DvrSeriesSchedulesFragment - .SERIES_SCHEDULES_KEY_SERIES_RECORDING); - // To get programs faster, hold the update of the series schedules. - SeriesRecordingScheduler.getInstance(this).pauseUpdate(); - new EpisodicProgramLoadTask(this, Collections.singletonList(seriesRecording)) { - @Override - protected void onPostExecute(List<Program> programs) { - SeriesRecordingScheduler.getInstance(DvrSchedulesActivity.this) - .resumeUpdate(); - dialog.dismiss(); - Bundle args = getIntent().getExtras(); - BigArguments.reset(); - BigArguments.setArgument( - DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, - programs == null ? Collections.EMPTY_LIST : programs); - showDvrSeriesSchedulesFragment(args); - } - }.setLoadCurrentProgram(true) - .setLoadDisallowedProgram(true) - .setLoadScheduledEpisode(true) - .setIgnoreChannelOption(true) - .execute(); - } + final ProgressDialog dialog = ProgressDialog.show(this, null, getString( + R.string.dvr_series_schedules_progress_message_reading_programs)); + SeriesRecording seriesRecording = getIntent().getExtras() + .getParcelable(DvrSeriesSchedulesFragment + .SERIES_SCHEDULES_KEY_SERIES_RECORDING); + // To get programs faster, hold the update of the series schedules. + SeriesRecordingScheduler.getInstance(this).pauseUpdate(); + new EpisodicProgramLoadTask(this, Collections.singletonList(seriesRecording)) { + @Override + protected void onPostExecute(List<Program> programs) { + SeriesRecordingScheduler.getInstance(DvrSchedulesActivity.this).resumeUpdate(); + dialog.dismiss(); + Bundle args = getIntent().getExtras(); + args.putParcelableArrayList(DvrSeriesSchedulesFragment + .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, new ArrayList<>(programs)); + DvrSeriesSchedulesFragment schedulesFragment = new DvrSeriesSchedulesFragment(); + schedulesFragment.setArguments(args); + getFragmentManager().beginTransaction().add( + R.id.fragment_container, schedulesFragment).commit(); + } + }.setLoadCurrentProgram(true) + .setLoadDisallowedProgram(true) + .setLoadScheduledEpisode(true) + .setIgnoreChannelOption(true) + .execute(); } else { finish(); } } - - private void showDvrSeriesSchedulesFragment(Bundle args) { - DvrSeriesSchedulesFragment schedulesFragment = new DvrSeriesSchedulesFragment(); - schedulesFragment.setArguments(args); - getFragmentManager().beginTransaction().add( - R.id.fragment_container, schedulesFragment).commit(); - } } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java index 667af34a..f57e4b05 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java @@ -22,6 +22,9 @@ import android.support.v17.leanback.app.GuidedStepFragment; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.dvr.ui.SeriesDeletionFragment; +import com.android.tv.ui.sidepanel.SettingsFragment; /** * Activity to show details view in DVR. @@ -39,7 +42,7 @@ public class DvrSeriesDeletionActivity extends Activity { setContentView(R.layout.activity_dvr_series_settings); // Check savedInstanceState to prevent that activity is being showed with animation. if (savedInstanceState == null) { - DvrSeriesDeletionFragment deletionFragment = new DvrSeriesDeletionFragment(); + SeriesDeletionFragment deletionFragment = new SeriesDeletionFragment(); deletionFragment.setArguments(getIntent().getExtras()); GuidedStepFragment.addAsRoot(this, deletionFragment, R.id.dvr_settings_view_frame); } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java index 8f880f16..1173df46 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java @@ -25,29 +25,22 @@ import android.support.v17.leanback.widget.GuidedAction; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.data.Program; +import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.SeriesRecording; -import com.android.tv.dvr.ui.list.DvrSchedulesActivity; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.SeriesRecording; import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; import java.util.List; public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { - /** - * The key for program list which will be passed to {@link DvrSeriesSchedulesFragment}. - * Type: List<{@link Program}> - */ - public static final String SERIES_SCHEDULED_KEY_PROGRAMS = "series_scheduled_key_programs"; - private final static long SERIES_RECORDING_ID_NOT_SET = -1; private final static int ACTION_VIEW_SCHEDULES = 1; + private DvrScheduleManager mDvrScheduleManager; private SeriesRecording mSeriesRecording; private boolean mShowViewScheduleOption; - private List<Program> mPrograms; private int mSchedulesAddedCount = 0; private boolean mHasConflict = false; @@ -65,25 +58,22 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { } mShowViewScheduleOption = getArguments().getBoolean( DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION); + mDvrScheduleManager = TvApplication.getSingletons(context).getDvrScheduleManager(); mSeriesRecording = TvApplication.getSingletons(context).getDvrDataManager() .getSeriesRecording(seriesRecordingId); if (mSeriesRecording == null) { getActivity().finish(); return; } - mPrograms = (List<Program>) BigArguments.getArgument(SERIES_SCHEDULED_KEY_PROGRAMS); - BigArguments.reset(); mSchedulesAddedCount = TvApplication.getSingletons(getContext()).getDvrManager() .getAvailableScheduledRecording(mSeriesRecording.getId()).size(); - DvrScheduleManager dvrScheduleManager = - TvApplication.getSingletons(context).getDvrScheduleManager(); List<ScheduledRecording> conflictingRecordings = - dvrScheduleManager.getConflictingSchedules(mSeriesRecording); + mDvrScheduleManager.getConflictingSchedules(mSeriesRecording); mHasConflict = !conflictingRecordings.isEmpty(); for (ScheduledRecording recording : conflictingRecordings) { if (recording.getSeriesRecordingId() == mSeriesRecording.getId()) { ++mInThisSeriesConflictCount; - } else if (recording.getPriority() < mSeriesRecording.getPriority()) { + } else { ++mOutThisSeriesConflictCount; } } @@ -123,9 +113,6 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { .TYPE_SERIES_SCHEDULE); intent.putExtra(DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING, mSeriesRecording); - BigArguments.reset(); - BigArguments.setArgument(DvrSeriesSchedulesFragment - .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, mPrograms); startActivity(intent); } getActivity().finish(); @@ -134,30 +121,30 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { private String getDescription() { if (!mHasConflict) { return getResources().getQuantityString( - R.plurals.dvr_series_scheduled_no_conflict, mSchedulesAddedCount, + R.plurals.dvr_series_recording_scheduled_no_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle()); } else { // mInThisSeriesConflictCount equals 0 and mOutThisSeriesConflictCount equals 0 means // mHasConflict is false. So we don't need to check that case. if (mInThisSeriesConflictCount != 0 && mOutThisSeriesConflictCount != 0) { - return getResources().getQuantityString( - R.plurals.dvr_series_scheduled_this_and_other_series_conflict, + return getResources().getQuantityString(R.plurals + .dvr_series_recording_scheduled_this_and_other_series_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), mInThisSeriesConflictCount + mOutThisSeriesConflictCount); } else if (mInThisSeriesConflictCount != 0) { - return getResources().getQuantityString( - R.plurals.dvr_series_recording_scheduled_only_this_series_conflict, + return getResources().getQuantityString(R.plurals + .dvr_series_recording_scheduled_only_this_series_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), mInThisSeriesConflictCount); } else { if (mOutThisSeriesConflictCount == 1) { - return getResources().getQuantityString( - R.plurals.dvr_series_scheduled_only_other_series_one_conflict, + return getResources().getQuantityString(R.plurals + .dvr_series_recording_scheduled_only_other_series_one_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle()); } else { - return getResources().getQuantityString( - R.plurals.dvr_series_scheduled_only_other_series_many_conflicts, + return getResources().getQuantityString(R.plurals + .dvr_series_recording_scheduled_only_other_series_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), mOutThisSeriesConflictCount); } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java index 6dd20b3a..3f7671b3 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java @@ -17,6 +17,7 @@ package com.android.tv.dvr.ui; import android.app.Activity; +import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; @@ -37,34 +38,25 @@ public class DvrSeriesSettingsActivity extends Activity { /** * Name of the boolean flag to decide if the series recording with empty schedule and recording * will be removed. - * Type: boolean */ public static final String REMOVE_EMPTY_SERIES_RECORDING = "remove_empty_series_recording"; /** * Name of the boolean flag to decide if the setting fragment should be translucent. - * Type: boolean */ public static final String IS_WINDOW_TRANSLUCENT = "windows_translucent"; /** - * Name of the program list. The list contains the programs which belong to the series. - * Type: List<{@link com.android.tv.data.Program}> + * Name of the channel id list. If the channel list is given, we show the channels + * from the values in channel option. + * Type: Long array */ - public static final String PROGRAM_LIST = "program_list"; + public static final String CHANNEL_ID_LIST = "channel_id_list"; /** * Name of the boolean flag to check if the confirm dialog should show view schedule option. - * Type: boolean */ public static final String SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG = "show_view_schedule_option_in_dialog"; - /** - * Name of the current program added to series. The current program will be recorded only when - * the series recording is initialized from media controller. But for other case, the current - * program won't be recorded. - */ - public static final String CURRENT_PROGRAM = "current_program"; - @Override public void onCreate(Bundle savedInstanceState) { TvApplication.setCurrentRunningProcess(this, true); @@ -74,7 +66,7 @@ public class DvrSeriesSettingsActivity extends Activity { SoftPreconditions.checkArgument(seriesRecordingId != -1); if (savedInstanceState == null) { - DvrSeriesSettingsFragment settingFragment = new DvrSeriesSettingsFragment(); + SeriesSettingsFragment settingFragment = new SeriesSettingsFragment(); settingFragment.setArguments(getIntent().getExtras()); GuidedStepFragment.addAsRoot(this, settingFragment, R.id.dvr_settings_view_frame); } diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java index b476fff7..c3867886 100644 --- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java @@ -33,7 +33,7 @@ import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ScheduledRecording; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -131,8 +131,15 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { String title = getString(R.string.dvr_stop_recording_dialog_title); String description; if (mStopReason == REASON_ON_CONFLICT) { + String programTitle = mSchedule.getProgramTitle(); + if (TextUtils.isEmpty(programTitle)) { + ChannelDataManager channelDataManager = + TvApplication.getSingletons(getActivity()).getChannelDataManager(); + Channel channel = channelDataManager.getChannel(mSchedule.getChannelId()); + programTitle = channel.getDisplayName(); + } description = getString(R.string.dvr_stop_recording_dialog_description_on_conflict, - mSchedule.getProgramDisplayTitle(getContext())); + mSchedule.getProgramTitle()); } else { description = getString(R.string.dvr_stop_recording_dialog_description); } diff --git a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java index fe3a4a60..feaa2357 100644 --- a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java @@ -31,8 +31,8 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.SeriesRecording; import java.util.ArrayList; import java.util.List; diff --git a/src/com/android/tv/dvr/ui/FadeBackground.java b/src/com/android/tv/dvr/ui/FadeBackground.java deleted file mode 100644 index 4f06ebcf..00000000 --- a/src/com/android/tv/dvr/ui/FadeBackground.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.animation.Animator; -import android.animation.ObjectAnimator; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.transition.Transition; -import android.transition.TransitionValues; -import android.transition.Visibility; -import android.util.AttributeSet; -import android.view.ViewGroup; - -import com.android.tv.R; - -/** - * This transition fades in/out of the background of the view by changing the background color. - */ -public class FadeBackground extends Transition { - private final int mMode; - - public FadeBackground(Context context, AttributeSet attrs) { - super(context, attrs); - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadeBackground); - mMode = a.getInt(R.styleable.FadeBackground_fadingMode, Visibility.MODE_IN); - a.recycle(); - } - - @Override - public void captureStartValues(TransitionValues transitionValues) { } - - @Override - public void captureEndValues(TransitionValues transitionValues) { } - - @Override - public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, - TransitionValues endValues) { - if (startValues == null || endValues == null) { - return null; - } - Drawable background = endValues.view.getBackground(); - if (background instanceof ColorDrawable) { - int color = ((ColorDrawable) background).getColor(); - int transparentColor = Color.argb(0, Color.red(color), Color.green(color), - Color.blue(color)); - return mMode == Visibility.MODE_OUT - ? ObjectAnimator.ofArgb(background, "color", transparentColor) - : ObjectAnimator.ofArgb(background, "color", transparentColor, color); - } - return null; - } -} diff --git a/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java b/src/com/android/tv/dvr/ui/FullScheduleCardHolder.java index 311137a9..d4d4d8ab 100644 --- a/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java +++ b/src/com/android/tv/dvr/ui/FullScheduleCardHolder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; /** * Special object for schedule preview; diff --git a/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java b/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java index 6d4763d4..7dd85f45 100644 --- a/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java +++ b/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; import android.content.Context; -import android.graphics.drawable.Drawable; +import android.support.v17.leanback.widget.Presenter; import android.view.View; import android.view.ViewGroup; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.util.Utils; import java.util.Collections; @@ -33,31 +33,23 @@ import java.util.List; /** * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. */ -class FullSchedulesCardPresenter extends DvrItemPresenter { - private Context mContext; - private final Drawable mIconDrawable; - private final String mCardTitleText; - - public FullSchedulesCardPresenter(Context context) { - mContext = context; - mIconDrawable = mContext.getDrawable(R.drawable.dvr_full_schedule); - mCardTitleText = mContext.getString(R.string.dvr_full_schedule_card_view_title); - } - +public class FullSchedulesCardPresenter extends Presenter { @Override public ViewHolder onCreateViewHolder(ViewGroup parent) { Context context = parent.getContext(); RecordingCardView view = new RecordingCardView(context); - return new ViewHolder(view); + return new ScheduledRecordingViewHolder(view); } @Override - public void onBindViewHolder(ViewHolder vh, Object o) { - final RecordingCardView cardView = (RecordingCardView) vh.view; + public void onBindViewHolder(ViewHolder baseHolder, Object o) { + final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; + final RecordingCardView cardView = (RecordingCardView) viewHolder.view; + final Context context = viewHolder.view.getContext(); - cardView.setImage(mIconDrawable); - cardView.setTitle(mCardTitleText); - List<ScheduledRecording> scheduledRecordings = TvApplication.getSingletons(mContext) + cardView.setImage(context.getDrawable(R.drawable.dvr_full_schedule)); + cardView.setTitle(context.getString(R.string.dvr_full_schedule_card_view_title)); + List<ScheduledRecording> scheduledRecordings = TvApplication.getSingletons(context) .getDvrDataManager().getAvailableScheduledRecordings(); int fullDays = 0; if (!scheduledRecordings.isEmpty()) { @@ -65,24 +57,28 @@ class FullSchedulesCardPresenter extends DvrItemPresenter { Collections.max(scheduledRecordings, ScheduledRecording.START_TIME_COMPARATOR) .getStartTimeMs()) + 1; } - cardView.setContent(mContext.getResources().getQuantityString( + cardView.setContent(context.getResources().getQuantityString( R.plurals.dvr_full_schedule_card_view_content, fullDays, fullDays), null); - super.onBindViewHolder(vh, o); + + View.OnClickListener clickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + DvrUiHelper.startSchedulesActivity(context, null); + } + }; + baseHolder.view.setOnClickListener(clickListener); } @Override - public void onUnbindViewHolder(ViewHolder vh) { - ((RecordingCardView) vh.view).reset(); - super.onUnbindViewHolder(vh); + public void onUnbindViewHolder(ViewHolder baseHolder) { + ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; + final RecordingCardView cardView = (RecordingCardView) viewHolder.view; + cardView.reset(); } - @Override - protected View.OnClickListener onCreateOnClickListener() { - return new View.OnClickListener() { - @Override - public void onClick(View view) { - DvrUiHelper.startSchedulesActivity(mContext, null); - } - }; + private static final class ScheduledRecordingViewHolder extends ViewHolder { + ScheduledRecordingViewHolder(RecordingCardView view) { + super(view); + } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dialog/HalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java index 315c6a93..d320816e 100644 --- a/src/com/android/tv/dialog/HalfSizedDialogFragment.java +++ b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package com.android.tv.dialog; +package com.android.tv.dvr.ui; -import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; import android.os.Handler; @@ -26,6 +25,7 @@ import android.view.View; import android.view.ViewGroup; import com.android.tv.R; +import com.android.tv.dialog.SafeDismissDialogFragment; import java.util.concurrent.TimeUnit; @@ -54,6 +54,13 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment { @Override public void onStart() { super.onStart(); + getDialog().setOnKeyListener(new DialogInterface.OnKeyListener() { + public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent keyEvent) { + mHandler.removeCallbacks(mAutoDismisser); + mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS); + return false; + } + }); mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS); } @@ -74,19 +81,6 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment { } @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - Dialog dialog = super.onCreateDialog(savedInstanceState); - dialog.setOnKeyListener(new DialogInterface.OnKeyListener() { - public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent keyEvent) { - mHandler.removeCallbacks(mAutoDismisser); - mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS); - return false; - } - }); - return dialog; - } - - @Override public int getTheme() { return R.style.Theme_TV_dialog_HalfSizedDialog; } diff --git a/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java b/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java index 562898a3..158bd824 100644 --- a/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java +++ b/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java @@ -33,7 +33,7 @@ import com.android.tv.TvApplication; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.SeriesRecording; import java.util.ArrayList; import java.util.List; @@ -41,7 +41,7 @@ import java.util.List; /** * Fragment for DVR series recording settings. */ -public class DvrPrioritySettingsFragment extends GuidedStepFragment { +public class PrioritySettingsFragment extends GuidedStepFragment { /** * Name of series recording id starting the fragment. * Type: Long @@ -162,6 +162,7 @@ public class DvrPrioritySettingsFragment extends GuidedStepFragment { return; } if (action.getId() < 0) { + int selectedPosition = mSeriesRecordings.indexOf(mSelectedRecording); mSelectedRecording = null; for (int i = 0; i < mSeriesRecordings.size(); ++i) { updateItem(i); @@ -247,4 +248,4 @@ public class DvrPrioritySettingsFragment extends GuidedStepFragment { titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL); } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java index fe9b9de5..e698b8a2 100644 --- a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; import android.content.res.Resources; import android.media.tv.TvInputManager; @@ -30,10 +30,10 @@ import com.android.tv.data.Channel; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.RecordedProgram; /** - * {@link android.support.v17.leanback.app.DetailsFragment} for recorded program in DVR. + * {@link DetailsFragment} for recorded program in DVR. */ public class RecordedProgramDetailsFragment extends DvrDetailsFragment implements DvrDataManager.RecordedProgramListener { diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java index ee978797..1bf34310 100644 --- a/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java +++ b/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java @@ -14,26 +14,31 @@ * limitations under the License. */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; +import android.app.Activity; import android.content.Context; import android.media.tv.TvContract; import android.media.tv.TvInputManager; +import android.net.Uri; import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.TextAppearanceSpan; +import android.view.View; import android.view.ViewGroup; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.dvr.RecordedProgram; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; -import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.util.Utils; +import java.util.concurrent.TimeUnit; + /** * Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}. */ @@ -45,7 +50,6 @@ public class RecordedProgramPresenter extends DvrItemPresenter { private String mYesterdayString; private final int mProgressBarColor; private final boolean mShowEpisodeTitle; - private final boolean mExpandTitleWhenFocused; private static final class RecordedProgramViewHolder extends ViewHolder implements WatchedPositionChangedListener { @@ -75,27 +79,25 @@ public class RecordedProgramPresenter extends DvrItemPresenter { } } - public RecordedProgramPresenter(Context context, boolean showEpisodeTitle, - boolean expandTitleWhenFocused) { + public RecordedProgramPresenter(Context context, boolean showEpisodeTitle) { mContext = context; - mChannelDataManager = TvApplication.getSingletons(mContext).getChannelDataManager(); - mTodayString = mContext.getString(R.string.dvr_date_today); - mYesterdayString = mContext.getString(R.string.dvr_date_yesterday); + mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager(); + mTodayString = context.getString(R.string.dvr_date_today); + mYesterdayString = context.getString(R.string.dvr_date_yesterday); mDvrWatchedPositionManager = - TvApplication.getSingletons(mContext).getDvrWatchedPositionManager(); - mProgressBarColor = mContext.getResources() + TvApplication.getSingletons(context).getDvrWatchedPositionManager(); + mProgressBarColor = context.getResources() .getColor(R.color.play_controls_progress_bar_watched); mShowEpisodeTitle = showEpisodeTitle; - mExpandTitleWhenFocused = expandTitleWhenFocused; } public RecordedProgramPresenter(Context context) { - this(context, false, false); + this(context, false); } @Override public ViewHolder onCreateViewHolder(ViewGroup parent) { - RecordingCardView view = new RecordingCardView(mContext, mExpandTitleWhenFocused); + RecordingCardView view = new RecordingCardView(mContext); return new RecordedProgramViewHolder(view, mProgressBarColor); } @@ -130,7 +132,8 @@ public class RecordedProgramPresenter extends DvrItemPresenter { isChannelLogo = true; } cardView.setImageUri(imageUri, isChannelLogo); - int durationMinutes = Math.max(1, Utils.getRoundOffMinsFromMs(program.getDurationMillis())); + int durationMinutes = + Math.max(1, (int) TimeUnit.MILLISECONDS.toMinutes(program.getDurationMillis())); String durationString = getContext().getResources().getQuantityString( R.plurals.dvr_program_duration, durationMinutes, durationMinutes); cardView.setContent(getDescription(program), durationString); diff --git a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java b/src/com/android/tv/dvr/ui/RecordingCardView.java index 7b0a8cb9..51c3b03b 100644 --- a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java +++ b/src/com/android/tv/dvr/ui/RecordingCardView.java @@ -14,69 +14,51 @@ * limitations under the License. */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; -import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Bitmap; -import android.graphics.Rect; 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.view.View; -import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; import com.android.tv.R; -import com.android.tv.dvr.data.RecordedProgram; -import com.android.tv.ui.ViewUtils; +import com.android.tv.dvr.RecordedProgram; import com.android.tv.util.ImageLoader; /** - * A CardView for displaying info about a {@link com.android.tv.dvr.data.ScheduledRecording} - * or {@link RecordedProgram} or {@link com.android.tv.dvr.data.SeriesRecording}. + * A CardView for displaying info about a {@link com.android.tv.dvr.ScheduledRecording} or + * {@link RecordedProgram} or + * {@link com.android.tv.dvr.SeriesRecording}. */ -public class RecordingCardView extends BaseCardView { - // This value should be the same with - // android.support.v17.leanback.widget.FocusHighlightHelper.BrowseItemFocusHighlight.DURATION_MS - private final static int ANIMATION_DURATION = 150; +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 mMajorContentView; private final TextView mMinorContentView; private final ProgressBar mProgressBar; private final View mAffiliatedIconContainer; private final ImageView mAffiliatedIcon; private final Drawable mDefaultImage; - private final FrameLayout mTitleArea; - private final TextView mFoldedTitleView; - private final TextView mExpandedTitleView; - private final ValueAnimator mExpandTitleAnimator; - private final int mFoldedTitleHeight; - private final int mExpandedTitleHeight; - private final boolean mExpandTitleWhenFocused; - private boolean mExpanded; - public RecordingCardView(Context context) { - this(context, false); + RecordingCardView(Context context) { + this(context, + context.getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_width), + context.getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_height)); } - public RecordingCardView(Context context, boolean expandTitleWhenFocused) { - this(context, context.getResources().getDimensionPixelSize( - R.dimen.dvr_library_card_image_layout_width), context.getResources() - .getDimensionPixelSize(R.dimen.dvr_library_card_image_layout_height), - expandTitleWhenFocused); - } - - public RecordingCardView(Context context, int imageWidth, int imageHeight, - boolean expandTitleWhenFocused) { + RecordingCardView(Context context, int imageWidth, int imageHeight) { super(context); //TODO(dvr): move these to the layout XML. setCardType(BaseCardView.CARD_TYPE_INFO_UNDER_WITH_EXTRA); @@ -93,73 +75,13 @@ public class RecordingCardView extends BaseCardView { mProgressBar = (ProgressBar) findViewById(R.id.recording_progress); mAffiliatedIconContainer = findViewById(R.id.affiliated_icon_container); mAffiliatedIcon = (ImageView) findViewById(R.id.affiliated_icon); + mTitleView = (TextView) findViewById(R.id.title); mMajorContentView = (TextView) findViewById(R.id.content_major); mMinorContentView = (TextView) findViewById(R.id.content_minor); - mTitleArea = (FrameLayout) findViewById(R.id.title_area); - mFoldedTitleView = (TextView) findViewById(R.id.title_one_line); - mExpandedTitleView = (TextView) findViewById(R.id.title_two_lines); - mFoldedTitleHeight = getResources() - .getDimensionPixelSize(R.dimen.dvr_library_card_folded_title_height); - mExpandedTitleHeight = getResources() - .getDimensionPixelSize(R.dimen.dvr_library_card_expanded_title_height); - mExpandTitleAnimator = ValueAnimator.ofFloat(0.0f, 1.0f).setDuration(ANIMATION_DURATION); - mExpandTitleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator valueAnimator) { - float value = (Float) valueAnimator.getAnimatedValue(); - mExpandedTitleView.setAlpha(value); - mFoldedTitleView.setAlpha(1.0f - value); - ViewUtils.setLayoutHeight(mTitleArea, (int) (mFoldedTitleHeight - + (mExpandedTitleHeight - mFoldedTitleHeight) * value)); - } - }); - mExpandTitleWhenFocused = expandTitleWhenFocused; - } - - @Override - protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { - super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); - if (mExpandTitleWhenFocused) { - if (gainFocus) { - expandTitle(true, true); - } else { - expandTitle(false, true); - } - } - } - - /** - * Expands/folds the title area to show program title with two/one lines. - * - * @param expand {@code true} to expand the title area, or {@code false} to fold it. - * @param withAnimation {@code true} to expand/fold with animation. - */ - public void expandTitle(boolean expand, boolean withAnimation) { - if (expand != mExpanded && mFoldedTitleView.getLayout().getEllipsisCount(0) > 0) { - if (withAnimation) { - if (expand) { - mExpandTitleAnimator.start(); - } else { - mExpandTitleAnimator.reverse(); - } - } else { - if (expand) { - mFoldedTitleView.setAlpha(0.0f); - mExpandedTitleView.setAlpha(1.0f); - ViewUtils.setLayoutHeight(mTitleArea, mExpandedTitleHeight); - } else { - mFoldedTitleView.setAlpha(1.0f); - mExpandedTitleView.setAlpha(0.0f); - ViewUtils.setLayoutHeight(mTitleArea, mFoldedTitleHeight); - } - } - mExpanded = expand; - } } void setTitle(CharSequence title) { - mFoldedTitleView.setText(title); - mExpandedTitleView.setText(title); + mTitleView.setText(title); } void setContent(CharSequence majorContent, CharSequence minorContent) { @@ -256,9 +178,8 @@ public class RecordingCardView extends BaseCardView { } public void reset() { - mFoldedTitleView.setText(null); - mExpandedTitleView.setText(null); + mTitleView.setText(null); setContent(null, null); mImageView.setImageDrawable(mDefaultImage); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java index a877e05f..4e19ec3f 100644 --- a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; import android.os.Bundle; import android.support.v17.leanback.app.DetailsFragment; @@ -26,7 +26,7 @@ import android.text.style.TextAppearanceSpan; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Channel; -import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ScheduledRecording; /** * {@link DetailsFragment} for recordings in DVR. diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java index eb0f4f0d..60816bb5 100644 --- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; import android.content.res.Resources; import android.os.Bundle; @@ -26,7 +26,7 @@ import android.text.TextUtils; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.dvr.DvrUiHelper; /** * {@link RecordingDetailsFragment} for scheduled recording in DVR. diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java index efc8785a..5f447f13 100644 --- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java +++ b/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java @@ -14,8 +14,9 @@ * limitations under the License. */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; +import android.app.Activity; import android.content.Context; import android.media.tv.TvContract; import android.os.Handler; @@ -31,7 +32,7 @@ import com.android.tv.TvApplication; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.util.Utils; import java.util.concurrent.TimeUnit; @@ -39,7 +40,7 @@ import java.util.concurrent.TimeUnit; /** * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. */ -class ScheduledRecordingPresenter extends DvrItemPresenter { +public class ScheduledRecordingPresenter extends DvrItemPresenter { private static final long PROGRESS_UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5); private final ChannelDataManager mChannelDataManager; @@ -91,17 +92,18 @@ class ScheduledRecordingPresenter extends DvrItemPresenter { } public ScheduledRecordingPresenter(Context context) { - mContext = context; - ApplicationSingletons singletons = TvApplication.getSingletons(mContext); + ApplicationSingletons singletons = TvApplication.getSingletons(context); mChannelDataManager = singletons.getChannelDataManager(); mDvrManager = singletons.getDvrManager(); - mProgressBarColor = mContext.getResources() + mContext = context; + mProgressBarColor = context.getResources() .getColor(R.color.play_controls_recording_icon_color_on_focus); } @Override public ViewHolder onCreateViewHolder(ViewGroup parent) { - RecordingCardView view = new RecordingCardView(mContext); + Context context = parent.getContext(); + RecordingCardView view = new RecordingCardView(context); return new ScheduledRecordingViewHolder(view, mProgressBarColor); } @@ -142,8 +144,9 @@ class ScheduledRecordingPresenter extends DvrItemPresenter { public void onUnbindViewHolder(ViewHolder baseHolder) { ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; viewHolder.stopUpdateProgressBar(); + final RecordingCardView cardView = (RecordingCardView) viewHolder.view; viewHolder.mScheduledRecording = null; - ((RecordingCardView) viewHolder.view).reset(); + cardView.reset(); super.onUnbindViewHolder(viewHolder); } @@ -171,4 +174,4 @@ class ScheduledRecordingPresenter extends DvrItemPresenter { cardView.setTitle(title); cardView.setImageUri(imageUri, isChannelLogo); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java index 8bf8560f..36e3cfc1 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java +++ b/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java @@ -33,10 +33,9 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.data.RecordedProgram; -import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.RecordedProgram; +import com.android.tv.dvr.SeriesRecording; import com.android.tv.ui.GuidedActionsStylistWithDivider; -import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.Collections; @@ -48,7 +47,7 @@ import java.util.concurrent.TimeUnit; /** * Fragment for DVR series recording settings. */ -public class DvrSeriesDeletionFragment extends GuidedStepFragment { +public class SeriesDeletionFragment extends GuidedStepFragment { private static final long WATCHED_TIME_UNIT_THRESHOLD = TimeUnit.MINUTES.toMillis(2); // Since recordings' IDs are used as its check actions' IDs, which are random positive numbers, @@ -219,8 +218,8 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment { private String getWatchedString(long watchedPositionMs, long durationMs) { if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) { return getResources().getString(R.string.dvr_series_watched_info_minutes, - Math.max(1, Utils.getRoundOffMinsFromMs(watchedPositionMs)), - Utils.getRoundOffMinsFromMs(durationMs)); + Math.max(1, TimeUnit.MILLISECONDS.toMinutes(watchedPositionMs)), + TimeUnit.MILLISECONDS.toMinutes(durationMs)); } else { return getResources().getString(R.string.dvr_series_watched_info_seconds, Math.max(1, TimeUnit.MILLISECONDS.toSeconds(watchedPositionMs)), diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java index f7b60b50..e9e391d4 100644 --- a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; import android.content.res.Resources; import android.graphics.drawable.Drawable; @@ -28,6 +28,7 @@ import android.support.v17.leanback.widget.DetailsOverviewRow; import android.support.v17.leanback.widget.DetailsOverviewRowPresenter; 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.OnActionClickedListener; import android.support.v17.leanback.widget.PresenterSelector; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; @@ -38,11 +39,12 @@ import com.android.tv.TvApplication; import com.android.tv.data.BaseProgram; import com.android.tv.data.Channel; import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrUiHelper; import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.data.RecordedProgram; -import com.android.tv.dvr.data.SeriesRecording; -import com.android.tv.dvr.ui.DvrUiHelper; -import com.android.tv.dvr.ui.SortedArrayAdapter; +import com.android.tv.dvr.RecordedProgram; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.SeriesRecording; import java.util.Collections; import java.util.Comparator; @@ -83,7 +85,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement mWatchLabel = getString(R.string.dvr_detail_watch); mResumeLabel = getString(R.string.dvr_detail_series_resume); mWatchDrawable = getResources().getDrawable(R.drawable.lb_ic_play, null); - mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true, true); + mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true); super.onCreate(savedInstanceState); } @@ -156,7 +158,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement DetailsOverviewRowPresenter rowPresenter) { ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); - presenterSelector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext())); + presenterSelector.addClassPresenter(ListRow.class, new ListRowPresenter()); return presenterSelector; } @@ -201,7 +203,10 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement mDvrDataManager.removeSeriesRecordingListener(this); mDvrDataManager.removeRecordedProgramListener(this); if (mSeries != null) { - mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeries.getId()); + DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); + if (dvrManager.canRemoveSeriesRecording(mSeries.getId())) { + dvrManager.removeSeriesRecording(mSeries.getId()); + } } mRecordedProgramPresenter.unbindAllViewHolders(); } @@ -260,6 +265,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { for (SeriesRecording series : seriesRecordings) { if (series.getId() == mSeries.getId()) { + mSeries = null; getActivity().finish(); return; } @@ -366,4 +372,4 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement return program.getId(); } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java b/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java index af6ecc19..c2c0f596 100644 --- a/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java +++ b/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java @@ -14,8 +14,9 @@ * limitations under the License. */ -package com.android.tv.dvr.ui.browse; +package com.android.tv.dvr.ui; +import android.app.Activity; import android.content.Context; import android.media.tv.TvContract; import android.media.tv.TvInputManager; @@ -33,16 +34,16 @@ import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; -import com.android.tv.dvr.data.RecordedProgram; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.RecordedProgram; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.SeriesRecording; import java.util.List; /** * Presents a {@link SeriesRecording} in {@link DvrBrowseFragment}. */ -class SeriesRecordingPresenter extends DvrItemPresenter { +public class SeriesRecordingPresenter extends DvrItemPresenter { private final ChannelDataManager mChannelDataManager; private final DvrDataManager mDvrDataManager; private final DvrManager mDvrManager; diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java index f28382da..6c05c9c6 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java +++ b/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java @@ -17,13 +17,19 @@ package com.android.tv.dvr.ui; import android.app.FragmentManager; +import android.app.ProgressDialog; import android.content.Context; 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 android.support.v17.leanback.widget.GuidedActionsStylist; +import android.util.Log; import android.util.LongSparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ProgressBar; import com.android.tv.R; import com.android.tv.TvApplication; @@ -32,14 +38,14 @@ import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.SeasonEpisodeNumber; -import com.android.tv.dvr.data.SeriesRecording; -import com.android.tv.dvr.data.SeriesRecording.ChannelOption; -import com.android.tv.dvr.recorder.SeriesRecordingScheduler; - +import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.EpisodicProgramLoadTask; +import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.SeriesRecording.ChannelOption; +import com.android.tv.dvr.SeriesRecordingScheduler; +import com.android.tv.dvr.SeriesRecordingScheduler.OnSeriesRecordingUpdatedListener; import java.util.ArrayList; -import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -47,7 +53,7 @@ import java.util.Set; /** * Fragment for DVR series recording settings. */ -public class DvrSeriesSettingsFragment extends GuidedStepFragment +public class SeriesSettingsFragment extends GuidedStepFragment implements DvrDataManager.SeriesRecordingListener { private static final String TAG = "SeriesSettingsFragment"; private static final boolean DEBUG = false; @@ -60,13 +66,15 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment private static final long SUB_ACTION_ID_CHANNEL_ONE_BASE = 500; private DvrDataManager mDvrDataManager; + private ChannelDataManager mChannelDataManager; + private DvrManager mDvrManager; private SeriesRecording mSeriesRecording; private long mSeriesRecordingId; @ChannelOption int mChannelOption; + private Comparator<Channel> mChannelComparator; private long mSelectedChannelId; private int mBackStackCount; private boolean mShowViewScheduleOptionInDialog; - private Program mCurrentProgram; private String mFragmentTitle; private String mProrityActionTitle; @@ -76,7 +84,7 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment private String mChannelsActionAllText; private LongSparseArray<Channel> mId2Channel = new LongSparseArray<>(); private List<Channel> mChannels = new ArrayList<>(); - private List<Program> mPrograms; + private EpisodicProgramLoadTask mEpisodicProgramLoadTask; private GuidedAction mPriorityGuidedAction; private GuidedAction mChannelsGuidedAction; @@ -92,24 +100,22 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment getActivity().finish(); return; } + mDvrManager = TvApplication.getSingletons(context).getDvrManager(); mShowViewScheduleOptionInDialog = getArguments().getBoolean( DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG); - mCurrentProgram = getArguments().getParcelable(DvrSeriesSettingsActivity.CURRENT_PROGRAM); mDvrDataManager.addSeriesRecordingListener(this); - mPrograms = (List<Program>) BigArguments.getArgument( - DvrSeriesSettingsActivity.PROGRAM_LIST); - BigArguments.reset(); - if (mPrograms == null) { - getActivity().finish(); - return; - } - Set<Long> channelIds = new HashSet<>(); - ChannelDataManager channelDataManager = - TvApplication.getSingletons(context).getChannelDataManager(); - for (Program program : mPrograms) { - long channelId = program.getChannelId(); - if (channelIds.add(channelId)) { - Channel channel = channelDataManager.getChannel(channelId); + long[] channelIds = getArguments().getLongArray(DvrSeriesSettingsActivity.CHANNEL_ID_LIST); + mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager(); + if (channelIds == null) { + Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId()); + if (channel != null) { + mId2Channel.put(channel.getId(), channel); + mChannels.add(channel); + } + collectChannelsInBackground(); + } else { + for (long channelId : channelIds) { + Channel channel = mChannelDataManager.getChannel(channelId); if (channel != null) { mId2Channel.put(channel.getId(), channel); mChannels.add(channel); @@ -119,14 +125,16 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment mChannelOption = mSeriesRecording.getChannelOption(); mSelectedChannelId = Channel.INVALID_ID; if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE) { - Channel channel = channelDataManager.getChannel(mSeriesRecording.getChannelId()); + Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId()); if (channel != null) { mSelectedChannelId = channel.getId(); } else { mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; } } - mChannels.sort(Channel.CHANNEL_NUMBER_COMPARATOR); + mChannelComparator = new Channel.DefaultComparator(context, + TvApplication.getSingletons(context).getTvInputManagerHelper()); + mChannels.sort(mChannelComparator); mFragmentTitle = getString(R.string.dvr_series_settings_title); mProrityActionTitle = getString(R.string.dvr_series_settings_priority); mProrityActionHighestText = getString(R.string.dvr_series_settings_priority_highest); @@ -136,23 +144,23 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment } @Override - public void onResume() { - super.onResume(); - // To avoid the order of series's priority has changed, but series doesn't get update. - updatePriorityGuidedAction(); - } - - @Override public void onDetach() { super.onDetach(); mDvrDataManager.removeSeriesRecordingListener(this); + if (mEpisodicProgramLoadTask != null) { + mEpisodicProgramLoadTask.cancel(true); + mEpisodicProgramLoadTask = null; + } } @Override public void onDestroy() { - if (getFragmentManager().getBackStackEntryCount() == mBackStackCount && getArguments() - .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING)) { - mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeriesRecordingId); + DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); + if (getFragmentManager().getBackStackEntryCount() == mBackStackCount + && getArguments() + .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING) + && dvrManager.canRemoveSeriesRecording(mSeriesRecordingId)) { + dvrManager.removeSeriesRecording(mSeriesRecordingId); } super.onDestroy(); } @@ -170,6 +178,7 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment .id(ACTION_ID_PRIORITY) .title(mProrityActionTitle) .build(); + updatePriorityGuidedAction(false); actions.add(mPriorityGuidedAction); mChannelsGuidedAction = new GuidedAction.Builder(getActivity()) @@ -195,6 +204,10 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment public void onGuidedActionClicked(GuidedAction action) { long actionId = action.getId(); if (actionId == GuidedAction.ACTION_ID_OK) { + if (mEpisodicProgramLoadTask != null) { + mEpisodicProgramLoadTask.cancel(true); + mEpisodicProgramLoadTask = null; + } if (mChannelOption != mSeriesRecording.getChannelOption() || mSeriesRecording.isStopped() || (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE @@ -205,14 +218,28 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment if (mSelectedChannelId != Channel.INVALID_ID) { builder.setChannelId(mSelectedChannelId); } - DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - dvrManager.updateSeriesRecording(builder.build()); - if (mCurrentProgram != null && (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL - || mSelectedChannelId == mCurrentProgram.getChannelId())) { - dvrManager.addSchedule(mCurrentProgram); - } - updateSchedulesToSeries(); - showConfirmDialog(); + TvApplication.getSingletons(getContext()).getDvrManager() + .updateSeriesRecording(builder.build()); + SeriesRecordingScheduler scheduler = + SeriesRecordingScheduler.getInstance(getContext()); + // Since dialog is used even after the fragment is closed, we should + // use application context. + ProgressDialog dialog = ProgressDialog.show(getContext(), null, getString( + R.string.dvr_series_schedules_progress_message_updating_programs)); + scheduler.addOnSeriesRecordingUpdatedListener( + new OnSeriesRecordingUpdatedListener() { + @Override + public void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings) { + for (SeriesRecording seriesRecording : seriesRecordings) { + if (seriesRecording.getId() == mSeriesRecordingId) { + dialog.dismiss(); + scheduler.removeOnSeriesRecordingUpdatedListener(this); + showConfirmDialog(); + return; + } + } + } + }); } else { showConfirmDialog(); } @@ -220,9 +247,9 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment finishGuidedStepFragments(); } else if (actionId == ACTION_ID_PRIORITY) { FragmentManager fragmentManager = getFragmentManager(); - DvrPrioritySettingsFragment fragment = new DvrPrioritySettingsFragment(); + PrioritySettingsFragment fragment = new PrioritySettingsFragment(); Bundle args = new Bundle(); - args.putLong(DvrPrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID, + args.putLong(PrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID, mSeriesRecording.getId()); fragment.setArguments(args); GuidedStepFragment.add(fragmentManager, fragment, R.id.dvr_settings_view_frame); @@ -254,7 +281,7 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment private void updateChannelsGuidedAction(boolean notifyActionChanged) { if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL) { mChannelsGuidedAction.setDescription(mChannelsActionAllText); - } else if (mId2Channel.get(mSelectedChannelId) != null){ + } else { mChannelsGuidedAction.setDescription(mId2Channel.get(mSelectedChannelId) .getDisplayText()); } @@ -263,7 +290,7 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment } } - private void updatePriorityGuidedAction() { + private void updatePriorityGuidedAction(boolean notifyActionChanged) { int totalSeriesCount = 0; int priorityOrder = 0; for (SeriesRecording seriesRecording : mDvrDataManager.getSeriesRecordings()) { @@ -285,38 +312,49 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment mPriorityGuidedAction.setDescription(getString( R.string.dvr_series_settings_priority_rank, priorityOrder + 1)); } - notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY)); + if (notifyActionChanged) { + notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY)); + } } - private void updateSchedulesToSeries() { - List<Program> recordingCandidates = new ArrayList<>(); - Set<SeasonEpisodeNumber> scheduledEpisodes = new HashSet<>(); - for (ScheduledRecording r : mDvrDataManager.getScheduledRecordings(mSeriesRecordingId)) { - if (r.getState() != ScheduledRecording.STATE_RECORDING_FAILED - && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) { - scheduledEpisodes.add(new SeasonEpisodeNumber( - r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber())); - } + private void collectChannelsInBackground() { + if (mEpisodicProgramLoadTask != null) { + mEpisodicProgramLoadTask.cancel(true); } - for (Program program : mPrograms) { - // Removes current programs and scheduled episodes out, matches the channel option. - if (program.getStartTimeUtcMillis() >= System.currentTimeMillis() - && mSeriesRecording.matchProgram(program) - && !scheduledEpisodes.contains(new SeasonEpisodeNumber( - mSeriesRecordingId, program.getSeasonNumber(), program.getEpisodeNumber()))) { - recordingCandidates.add(program); + mEpisodicProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) { + @Override + protected void onPostExecute(List<Program> programs) { + mEpisodicProgramLoadTask = null; + Set<Long> channelIds = new HashSet<>(); + for (Program program : programs) { + channelIds.add(program.getChannelId()); + } + boolean channelAdded = false; + for (Long channelId : channelIds) { + if (mId2Channel.get(channelId) != null) { + continue; + } + Channel channel = mChannelDataManager.getChannel(channelId); + if (channel != null) { + channelAdded = true; + mId2Channel.put(channelId, channel); + mChannels.add(channel); + if (DEBUG) Log.d(TAG, "Added channel: " + channel); + } + } + if (!channelAdded) { + return; + } + mChannels.sort(mChannelComparator); + mChannelsGuidedAction.setSubActions(buildChannelSubAction()); + notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL)); + if (DEBUG) Log.d(TAG, "Complete EpisodicProgramLoadTask"); } - } - if (recordingCandidates.isEmpty()) { - return; - } - List<Program> programsToSchedule = SeriesRecordingScheduler.pickOneProgramPerEpisode( - mDvrDataManager, Collections.singletonList(mSeriesRecording), recordingCandidates) - .get(mSeriesRecordingId); - if (!programsToSchedule.isEmpty()) { - TvApplication.getSingletons(getContext()).getDvrManager() - .addScheduleToSeriesRecording(mSeriesRecording, programsToSchedule); - } + }.setLoadCurrentProgram(true) + .setLoadDisallowedProgram(true) + .setLoadScheduledEpisode(true) + .setIgnoreChannelOption(true); + mEpisodicProgramLoadTask.execute(); } private List<GuidedAction> buildChannelSubAction() { @@ -335,8 +373,8 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment } private void showConfirmDialog() { - DvrUiHelper.StartSeriesScheduledDialogActivity(getContext(), mSeriesRecording, - mShowViewScheduleOptionInDialog, mPrograms); + DvrUiHelper.StartSeriesScheduledDialogActivity( + getContext(), mSeriesRecording, mShowViewScheduleOptionInDialog); finishGuidedStepFragments(); } @@ -344,23 +382,16 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } @Override - public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { - for (SeriesRecording series : seriesRecordings) { - if (series.getId() == mSeriesRecording.getId()) { - finishGuidedStepFragments(); - return; - } - } - } + public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { } @Override public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { for (SeriesRecording seriesRecording : seriesRecordings) { if (seriesRecording.getId() == mSeriesRecordingId) { mSeriesRecording = seriesRecording; - updatePriorityGuidedAction(); + updatePriorityGuidedAction(true); return; } } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java index 8c0af9ed..393a5ff3 100644 --- a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java +++ b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java @@ -20,15 +20,11 @@ import android.support.annotation.VisibleForTesting; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.PresenterSelector; -import com.android.tv.common.SoftPreconditions; - import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; -import java.util.HashSet; import java.util.List; -import java.util.Set; /** * Keeps a set of items sorted @@ -39,18 +35,16 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { private final Comparator<T> mComparator; private final int mMaxItemCount; private int mExtraItemCount; - private final Set<Long> mIds = new HashSet<>(); - public SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator) { + SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator) { this(presenterSelector, comparator, Integer.MAX_VALUE); } - public SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator, + SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator, int maxItemCount) { super(presenterSelector); mComparator = comparator; mMaxItemCount = maxItemCount; - setHasStableIds(true); } /** @@ -62,12 +56,7 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { final void setInitialItems(List<T> items) { List<T> itemsCopy = new ArrayList<>(items); Collections.sort(itemsCopy, mComparator); - for (T item : itemsCopy) { - add(item, true); - if (size() == mMaxItemCount) { - break; - } - } + addAll(0, itemsCopy.subList(0, Math.min(mMaxItemCount, itemsCopy.size()))); } /** @@ -93,9 +82,6 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { * the end to save search time. */ public final void add(T item, boolean insertToEnd) { - long newItemId = getId(item); - SoftPreconditions.checkState(!mIds.contains(newItemId)); - mIds.add(newItemId); int i; if (insertToEnd) { i = findInsertPosition(item); @@ -103,9 +89,8 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { i = findInsertPositionBinary(item); } super.add(i, item); - if (mMaxItemCount < Integer.MAX_VALUE && size() > mMaxItemCount + mExtraItemCount) { - Object removedItem = get(mMaxItemCount); - remove(removedItem); + if (size() > mMaxItemCount + mExtraItemCount) { + removeItems(mMaxItemCount, size() - mMaxItemCount - mExtraItemCount); } } @@ -115,97 +100,48 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { * They will be presented in their insertion order. */ public int addExtraItem(T item) { - long newItemId = getId(item); - SoftPreconditions.checkState(!mIds.contains(newItemId)); - mIds.add(newItemId); super.add(item); return ++mExtraItemCount; } - @Override - public boolean remove(Object item) { - return removeWithId((T) item); - } - /** * Removes an item which has the same ID as {@code item}. */ public boolean removeWithId(T item) { - int index = indexWithId(item); - return index >= 0 && index < size() && removeItems(index, 1) == 1; - } - - @Override - public int removeItems(int position, int count) { - int upperBound = Math.min(position + count, size()); - for (int i = position; i < upperBound; i++) { - mIds.remove(getId((T) get(i))); - } - if (upperBound > size() - mExtraItemCount) { - mExtraItemCount -= upperBound - Math.max(size() - mExtraItemCount, position); - } - return super.removeItems(position, count); - } - - @Override - public void replace(int position, Object item) { - boolean wasExtra = position >= size() - mExtraItemCount; - removeItems(position, 1); - if (!wasExtra) { - add(item); - } else { - addExtraItem((T) item); - } - } - - @Override - public void clear() { - mIds.clear(); - super.clear(); + int index = indexWithTypeAndId(item); + return index >= 0 && index < size() && remove(get(index)); } /** - * Changes an item in the list. + * Change an item in the list. * @param item The item to change. */ public final void change(T item) { - int oldIndex = indexWithId(item); + int oldIndex = indexWithTypeAndId(item); if (oldIndex != -1) { T old = (T) get(oldIndex); if (mComparator.compare(old, item) == 0) { replace(oldIndex, item); return; } - remove(old); + removeItems(oldIndex, 1); } add(item); } /** - * Checks whether the item is in the list. - */ - public final boolean contains(T item) { - return indexWithId(item) != -1; - } - - @Override - public long getId(int position) { - return getId((T) get(position)); - } - - /** * Returns the id of the the given {@code item}, which will be used in {@link #change} to * decide if the given item is already existed in the adapter. * * The id must be stable. */ - protected abstract long getId(T item); + abstract long getId(T item); - private int indexWithId(T item) { + private int indexWithTypeAndId(T item) { long id = getId(item); for (int i = 0; i < size() - mExtraItemCount; i++) { T r = (T) get(i); - if (getId(r) == id) { + if (r.getClass() == item.getClass() && getId(r) == id) { return i; } } diff --git a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java deleted file mode 100644 index c8f6a03f..00000000 --- a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui.browse; - -import android.content.Context; -import android.content.res.Resources; -import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.OnActionClickedListener; -import android.support.v17.leanback.widget.SparseArrayObjectAdapter; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.dialog.HalfSizedDialogFragment; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.ui.DvrStopRecordingFragment; -import com.android.tv.dvr.ui.DvrUiHelper; - -/** - * {@link RecordingDetailsFragment} for current recording in DVR. - */ -public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { - private static final int ACTION_STOP_RECORDING = 1; - - private DvrDataManager mDvrDataManger; - private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener = - new DvrDataManager.ScheduledRecordingListener() { - @Override - public void onScheduledRecordingAdded(ScheduledRecording... schedules) { } - - @Override - public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { - for (ScheduledRecording schedule : schedules) { - if (schedule.getId() == getRecording().getId()) { - getActivity().finish(); - return; - } - } - } - - @Override - public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { - for (ScheduledRecording schedule : schedules) { - if (schedule.getId() == getRecording().getId() - && schedule.getState() - != ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - getActivity().finish(); - return; - } - } - } - }; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - mDvrDataManger = TvApplication.getSingletons(context).getDvrDataManager(); - mDvrDataManger.addScheduledRecordingListener(mScheduledRecordingListener); - } - - @Override - protected SparseArrayObjectAdapter onCreateActionsAdapter() { - SparseArrayObjectAdapter adapter = - new SparseArrayObjectAdapter(new ActionPresenterSelector()); - Resources res = getResources(); - adapter.set(ACTION_STOP_RECORDING, new Action(ACTION_STOP_RECORDING, - res.getString(R.string.epg_dvr_dialog_message_stop_recording), null, - res.getDrawable(R.drawable.lb_ic_stop))); - return adapter; - } - - @Override - protected OnActionClickedListener onCreateOnActionClickedListener() { - return new OnActionClickedListener() { - @Override - public void onActionClicked(Action action) { - if (action.getId() == ACTION_STOP_RECORDING) { - DvrUiHelper.showStopRecordingDialog(getActivity(), - getRecording().getChannelId(), - DvrStopRecordingFragment.REASON_USER_STOP, - new HalfSizedDialogFragment.OnActionClickListener() { - @Override - public void onActionClick(long actionId) { - if (actionId == DvrStopRecordingFragment.ACTION_STOP) { - DvrManager dvrManager = - TvApplication.getSingletons(getContext()) - .getDvrManager(); - dvrManager.stopRecording(getRecording()); - getActivity().finish(); - } - } - }); - } - } - }; - } - - @Override - public void onDetach() { - if (mDvrDataManger != null) { - mDvrDataManger.removeScheduledRecordingListener(mScheduledRecordingListener); - } - super.onDetach(); - } -} diff --git a/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java deleted file mode 100644 index 37a72eaf..00000000 --- a/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui.browse; - -import android.content.Context; -import android.support.v17.leanback.widget.ListRowPresenter; -import android.view.ViewGroup; - -import com.android.tv.R; - -/** A list row presenter to display expand/fold card views list. */ -public class DvrListRowPresenter extends ListRowPresenter { - public DvrListRowPresenter(Context context) { - super(); - setRowHeight(ViewGroup.LayoutParams.WRAP_CONTENT); - setExpandedRowHeight( - context.getResources() - .getDimensionPixelSize(R.dimen.dvr_library_expanded_row_height)); - } -} diff --git a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java index 5abd52a1..d28f026c 100644 --- a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java @@ -29,7 +29,7 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ScheduledRecording; /** * A base fragment to show the list of schedule recordings. diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java index 3cbb500a..722c9b6e 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java @@ -18,9 +18,12 @@ package com.android.tv.dvr.ui.list; import android.os.Bundle; import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; import com.android.tv.R; -import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.DateHeaderRowPresenter; /** diff --git a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java index 57e7a88f..42a1e72b 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java @@ -17,7 +17,6 @@ package com.android.tv.dvr.ui.list; import android.annotation.TargetApi; -import android.content.Context; import android.database.ContentObserver; import android.media.tv.TvContract.Programs; import android.net.Uri; @@ -36,13 +35,11 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; -import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; -import com.android.tv.dvr.data.SeriesRecording; -import com.android.tv.dvr.provider.EpisodicProgramLoadTask; -import com.android.tv.dvr.ui.BigArguments; +import com.android.tv.dvr.EpisodicProgramLoadTask; +import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.SeriesRecordingHeaderRowPresenter; -import java.util.Collections; import java.util.List; /** @@ -50,22 +47,20 @@ import java.util.List; */ @TargetApi(Build.VERSION_CODES.N) public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { + private static final String TAG = "DvrSeriesSchedulesFragment"; /** * The key for series recording whose scheduled recording list will be displayed. - * Type: {@link SeriesRecording} */ public static final String SERIES_SCHEDULES_KEY_SERIES_RECORDING = "series_schedules_key_series_recording"; /** - * The key for programs which belong to the series recording whose scheduled recording list - * will be displayed. - * Type: List<{@link Program}> + * The key for programs belong to the series recording whose scheduled recording + * list will be displayed. */ public static final String SERIES_SCHEDULES_KEY_SERIES_PROGRAMS = "series_schedules_key_series_programs"; private ChannelDataManager mChannelDataManager; - private DvrDataManager mDvrDataManager; private SeriesRecording mSeriesRecording; private List<Program> mPrograms; private EpisodicProgramLoadTask mProgramLoadTask; @@ -92,22 +87,20 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { && getRowsAdapter() instanceof SeriesScheduleRowAdapter) { ((SeriesScheduleRowAdapter) getRowsAdapter()) .onSeriesRecordingUpdated(r); - mSeriesRecording = r; - updateEmptyMessage(); return; } } } }; - private final Handler mHandler = new Handler(Looper.getMainLooper()); - private final ContentObserver mContentObserver = new ContentObserver(mHandler) { - @Override - public void onChange(boolean selfChange, Uri uri) { - super.onChange(selfChange, uri); - executeProgramLoadingTask(); - } - }; + private final ContentObserver mContentObserver = + new ContentObserver(new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + executeProgramLoadingTask(); + } + }; private final ChannelDataManager.Listener mChannelListener = new ChannelDataManager.Listener() { @Override @@ -127,28 +120,17 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { } @Override - public void onAttach(Context context) { - super.onAttach(context); + public void onCreate(Bundle savedInstanceState) { Bundle args = getArguments(); if (args != null) { mSeriesRecording = args.getParcelable(SERIES_SCHEDULES_KEY_SERIES_RECORDING); - mPrograms = (List<Program>) BigArguments.getArgument( - SERIES_SCHEDULES_KEY_SERIES_PROGRAMS); - BigArguments.reset(); + mPrograms = args.getParcelableArrayList(SERIES_SCHEDULES_KEY_SERIES_PROGRAMS); } - if (args == null || mPrograms == null) { - getActivity().finish(); - } - } - - @Override - public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); + singletons.getDvrDataManager().addSeriesRecordingListener(mSeriesRecordingListener); mChannelDataManager = singletons.getChannelDataManager(); mChannelDataManager.addListener(mChannelListener); - mDvrDataManager = singletons.getDvrDataManager(); - mDvrDataManager.addSeriesRecordingListener(mSeriesRecordingListener); getContext().getContentResolver().registerContentObserver(Programs.CONTENT_URI, true, mContentObserver); } @@ -162,16 +144,8 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { private void onProgramsUpdated() { ((SeriesScheduleRowAdapter) getRowsAdapter()).setPrograms(mPrograms); - updateEmptyMessage(); - } - - private void updateEmptyMessage() { if (mPrograms == null || mPrograms.isEmpty()) { - if (mSeriesRecording.getState() == SeriesRecording.STATE_SERIES_STOPPED) { - showEmptyMessage(R.string.dvr_series_schedules_stopped_empty_state); - } else { - showEmptyMessage(R.string.dvr_series_schedules_empty_state); - } + showEmptyMessage(R.string.dvr_series_schedules_empty_state); } else { hideEmptyMessage(); } @@ -184,15 +158,15 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { mProgramLoadTask = null; } getContext().getContentResolver().unregisterContentObserver(mContentObserver); - mHandler.removeCallbacksAndMessages(null); mChannelDataManager.removeListener(mChannelListener); - mDvrDataManager.removeSeriesRecordingListener(mSeriesRecordingListener); + TvApplication.getSingletons(getContext()).getDvrDataManager() + .removeSeriesRecordingListener(mSeriesRecordingListener); super.onDestroy(); } @Override public SchedulesHeaderRowPresenter onCreateHeaderRowPresenter() { - return new SchedulesHeaderRowPresenter.SeriesRecordingHeaderRowPresenter(getContext()); + return new SeriesRecordingHeaderRowPresenter(getContext()); } @Override @@ -221,7 +195,7 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { mProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) { @Override protected void onPostExecute(List<Program> programs) { - mPrograms = programs == null ? Collections.EMPTY_LIST : programs; + mPrograms = programs; onProgramsUpdated(); } }; @@ -231,4 +205,4 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { .setIgnoreChannelOption(true) .execute(); } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java index 9b0ad105..23aebf59 100644 --- a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java +++ b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java @@ -19,13 +19,13 @@ package com.android.tv.dvr.ui.list; import android.content.Context; import com.android.tv.data.Program; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.ScheduledRecording.Builder; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.ScheduledRecording.Builder; /** * A class for the episodic program. */ -class EpisodicProgramRow extends ScheduleRow { +public class EpisodicProgramRow extends ScheduleRow { private final String mInputId; private final Program mProgram; diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRow.java b/src/com/android/tv/dvr/ui/list/ScheduleRow.java index 66e96f94..3fc92e8a 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRow.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRow.java @@ -20,12 +20,12 @@ import android.content.Context; import android.support.annotation.Nullable; import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ScheduledRecording; /** * A class for schedule recording row. */ -class ScheduleRow { +public class ScheduleRow { private final SchedulesHeaderRow mHeaderRow; @Nullable private ScheduledRecording mSchedule; private boolean mStopRecordingRequested; diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java index 97d60473..9cc82653 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java @@ -30,8 +30,8 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow; -import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Utils; import java.util.ArrayList; @@ -43,7 +43,7 @@ import java.util.concurrent.TimeUnit; /** * An adapter for {@link ScheduleRow}. */ -class ScheduleRowAdapter extends ArrayObjectAdapter { +public class ScheduleRowAdapter extends ArrayObjectAdapter { private static final String TAG = "ScheduleRowAdapter"; private static final boolean DEBUG = false; diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java index dc4e3c41..1257e725 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java @@ -42,24 +42,25 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; -import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.ui.DvrStopRecordingFragment; -import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.dvr.ui.HalfSizedDialogFragment; import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; +import java.util.concurrent.TimeUnit; /** * A RowPresenter for {@link ScheduleRow}. */ @TargetApi(Build.VERSION_CODES.N) -class ScheduleRowPresenter extends RowPresenter { +public class ScheduleRowPresenter extends RowPresenter { private static final String TAG = "ScheduleRowPresenter"; @Retention(RetentionPolicy.SOURCE) @@ -344,9 +345,7 @@ class ScheduleRowPresenter extends RowPresenter { viewHolder.mInfoContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - if (isInfoClickable(row)) { - onInfoClicked(row); - } + onInfoClicked(row); } }); @@ -367,7 +366,8 @@ class ScheduleRowPresenter extends RowPresenter { viewHolder.mTimeView.setText(onGetRecordingTimeText(row)); String programInfoText = onGetProgramInfoText(row); if (TextUtils.isEmpty(programInfoText)) { - int durationMins = Math.max(1, Utils.getRoundOffMinsFromMs(row.getDuration())); + int durationMins = + Math.max((int) TimeUnit.MILLISECONDS.toMinutes(row.getDuration()), 1); programInfoText = mContext.getResources().getQuantityString( R.plurals.dvr_schedules_recording_duration, durationMins, durationMins); } @@ -403,7 +403,6 @@ class ScheduleRowPresenter extends RowPresenter { } else { viewHolder.whiteBackInfo(); } - viewHolder.mInfoContainer.setFocusable(isInfoClickable(row)); updateActionContainer(viewHolder, viewHolder.isSelected()); } @@ -455,13 +454,11 @@ class ScheduleRowPresenter extends RowPresenter { /** * Called when user click Info in {@link ScheduleRow}. */ - protected void onInfoClicked(ScheduleRow row) { - DvrUiHelper.startDetailsActivity((Activity) mContext, row.getSchedule(), null, true); - } - - private boolean isInfoClickable(ScheduleRow row) { - return row.getSchedule() != null - && (row.getSchedule().isNotStarted() || row.getSchedule().isInProgress()); + protected void onInfoClicked(ScheduleRow scheduleRow) { + ScheduledRecording schedule = scheduleRow.getSchedule(); + if (schedule != null) { + DvrUiHelper.startDetailsActivity((Activity) mContext, schedule, null, true); + } } /** @@ -548,7 +545,7 @@ class ScheduleRowPresenter extends RowPresenter { // This row has been deleted. return; } - if (row.isRecordingInProgress() && !row.isStopRecordingRequested()) { + if (row.isOnAir() && row.isRecordingInProgress() && !row.isStopRecordingRequested()) { row.setStopRecordingRequested(true); mDvrManager.stopRecording(row.getSchedule()); CharSequence deletedInfo = onGetProgramInfoText(row); @@ -673,9 +670,10 @@ class ScheduleRowPresenter extends RowPresenter { hideActionView(viewHolder.mFirstActionContainer, View.GONE); } }; - mLastFocusedViewId = R.id.info_container; - SoftPreconditions.checkState(viewHolder.mInfoContainer.isFocusable(), TAG, - "No focusable view in this row: " + viewHolder); + if (mLastFocusedViewId == R.id.action_first_container + || mLastFocusedViewId == R.id.action_second_container) { + mLastFocusedViewId = R.id.info_container; + } break; } View view = viewHolder.view.findViewById(mLastFocusedViewId); @@ -685,10 +683,8 @@ class ScheduleRowPresenter extends RowPresenter { // requestFocus() explicitly. if (view.hasFocus()) { viewHolder.mPendingAnimationRunnable.run(); - } else if (view.isFocusable()){ - view.requestFocus(); } else { - viewHolder.view.requestFocus(); + view.requestFocus(); } } } else { @@ -741,10 +737,10 @@ class ScheduleRowPresenter extends RowPresenter { @ScheduleRowAction protected int[] getAvailableActions(ScheduleRow row) { if (row.getSchedule() != null) { - if (row.isRecordingInProgress()) { - return new int[]{ACTION_STOP_RECORDING}; - } else if (row.isOnAir()) { - if (row.isRecordingNotStarted()) { + if (row.isOnAir()) { + if (row.isRecordingInProgress()) { + return new int[] {ACTION_STOP_RECORDING}; + } else if (row.isRecordingNotStarted()) { if (canResolveConflict()) { // The "START" action can change the conflict states. return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_START_RECORDING}; diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java index 715ecb8c..0fb0924d 100644 --- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java +++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java @@ -16,15 +16,12 @@ package com.android.tv.dvr.ui.list; -import com.android.tv.data.Program; -import com.android.tv.dvr.data.SeriesRecording; - -import java.util.List; +import com.android.tv.dvr.SeriesRecording; /** * A base class for the rows for schedules' header. */ -abstract class SchedulesHeaderRow { +public abstract class SchedulesHeaderRow { private String mTitle; private String mDescription; private int mItemCount; @@ -101,20 +98,11 @@ abstract class SchedulesHeaderRow { */ public static class SeriesRecordingHeaderRow extends SchedulesHeaderRow { private SeriesRecording mSeriesRecording; - private List<Program> mPrograms; public SeriesRecordingHeaderRow(String title, String description, int itemCount, - SeriesRecording series, List<Program> programs) { + SeriesRecording series) { super(title, description, itemCount); mSeriesRecording = series; - mPrograms = programs; - } - - /** - * Returns the list of programs which belong to the series. - */ - public List<Program> getPrograms() { - return mPrograms; } /** @@ -131,4 +119,4 @@ abstract class SchedulesHeaderRow { mSeriesRecording = seriesRecording; } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java index fe2033ba..69c33a96 100644 --- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java @@ -30,14 +30,15 @@ import android.widget.TextView; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.data.SeriesRecording; -import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.ui.DvrSchedulesActivity; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; /** * A base class for RowPresenter for {@link SchedulesHeaderRow} */ -abstract class SchedulesHeaderRowPresenter extends RowPresenter { +public abstract class SchedulesHeaderRowPresenter extends RowPresenter { private Context mContext; public SchedulesHeaderRowPresenter(Context context) { @@ -78,7 +79,7 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter { } /** - * A presenter for {@link SchedulesHeaderRow.DateHeaderRow}. + * A presenter for {@link com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow}. */ public static class DateHeaderRowPresenter extends SchedulesHeaderRowPresenter { public DateHeaderRowPresenter(Context context) { @@ -92,7 +93,7 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter { /** * A ViewHolder for - * {@link SchedulesHeaderRow.DateHeaderRow}. + * {@link com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow}. */ public static class DateHeaderRowViewHolder extends SchedulesHeaderRowViewHolder { public DateHeaderRowViewHolder(Context context, ViewGroup parent) { @@ -151,9 +152,9 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter { headerViewHolder.mSeriesSettingsButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { + // TODO: pass channel list for settings. DvrUiHelper.startSeriesSettingsActivity(getContext(), - header.getSeriesRecording().getId(), - header.getPrograms(), false, false, false, null); + header.getSeriesRecording().getId(), null, false, false, false); } }); headerViewHolder.mToggleStartStopButton.setOnClickListener(new OnClickListener() { @@ -168,9 +169,9 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter { .build(); TvApplication.getSingletons(getContext()).getDvrManager() .updateSeriesRecording(seriesRecording); + // TODO: pass channel list for settings. DvrUiHelper.startSeriesSettingsActivity(getContext(), - header.getSeriesRecording().getId(), - header.getPrograms(), false, false, false, null); + header.getSeriesRecording().getId(), null, false, false, false); } else { DvrUiHelper.showCancelAllSeriesRecordingDialog( (DvrSchedulesActivity) view.getContext(), @@ -181,8 +182,11 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter { } private void setTextDrawable(TextView textView, Drawable drawableStart) { - textView.setCompoundDrawablesRelativeWithIntrinsicBounds(drawableStart, null, null, - null); + if (mLtr) { + textView.setCompoundDrawablesWithIntrinsicBounds(drawableStart, null, null, null); + } else { + textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawableStart, null); + } } /** diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java index 6b6de8b8..3b493774 100644 --- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java +++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java @@ -31,8 +31,8 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.SeriesRecording; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; import com.android.tv.util.Utils; @@ -46,7 +46,7 @@ import java.util.Map; * An adapter for series schedule row. */ @TargetApi(Build.VERSION_CODES.N) -class SeriesScheduleRowAdapter extends ScheduleRowAdapter { +public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { private static final String TAG = "SeriesRowAdapter"; private static final boolean DEBUG = false; @@ -96,7 +96,7 @@ class SeriesScheduleRowAdapter extends ScheduleRowAdapter { Collections.sort(sortedPrograms); List<EpisodicProgramRow> rows = new ArrayList<>(); mHeaderRow = new SeriesRecordingHeaderRow(mSeriesRecording.getTitle(), - null, sortedPrograms.size(), mSeriesRecording, programs); + null, sortedPrograms.size(), mSeriesRecording); for (Program program : sortedPrograms) { ScheduledRecording schedule = mDataManager.getScheduledRecordingForProgramId(program.getId()); @@ -145,7 +145,7 @@ class SeriesScheduleRowAdapter extends ScheduleRowAdapter { if (index != -1) { EpisodicProgramRow row = (EpisodicProgramRow) get(index); if (!row.isStartRecordingRequested()) { - setScheduleToRow(row, schedule); + row.setSchedule(schedule); notifyArrayItemRangeChanged(index, 1); } } @@ -195,10 +195,12 @@ class SeriesScheduleRowAdapter extends ScheduleRowAdapter { if (!isStartOrStopRequested()) { executePendingUpdate(); } - setScheduleToRow(row, schedule); + row.setSchedule(schedule); } + } else if (willBeKept(schedule)) { + row.setSchedule(schedule); } else { - setScheduleToRow(row, schedule); + row.setSchedule(null); } notifyArrayItemRangeChanged(index, 1); } @@ -211,14 +213,6 @@ class SeriesScheduleRowAdapter extends ScheduleRowAdapter { } } - private void setScheduleToRow(ScheduleRow row, ScheduledRecording schedule) { - if (schedule != null && willBeKept(schedule)) { - row.setSchedule(schedule); - } else { - row.setSchedule(null); - } - } - private int findRowIndexByProgramId(long programId) { for (int i = 0; i < size(); i++) { Object item = get(i); diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java index c8503e0d..5d88579a 100644 --- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java @@ -22,13 +22,13 @@ import android.view.ViewGroup; import com.android.tv.R; import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.dvr.DvrUiHelper; import com.android.tv.util.Utils; /** * A RowPresenter for series schedule row. */ -class SeriesScheduleRowPresenter extends ScheduleRowPresenter { +public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { private static final String TAG = "SeriesRowPresenter"; private boolean mLtr; @@ -74,8 +74,13 @@ class SeriesScheduleRowPresenter extends ScheduleRowPresenter { viewHolder.getProgramTitleView().setCompoundDrawablePadding(getContext() .getResources().getDimensionPixelOffset( R.dimen.dvr_schedules_warning_icon_padding)); - viewHolder.getProgramTitleView().setCompoundDrawablesRelativeWithIntrinsicBounds( - R.drawable.ic_warning_gray600_36dp, 0, 0, 0); + if (mLtr) { + viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_warning_gray600_36dp, 0, 0, 0); + } else { + viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds( + 0, 0, R.drawable.ic_warning_gray600_36dp, 0); + } } else { viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); } @@ -83,7 +88,9 @@ class SeriesScheduleRowPresenter extends ScheduleRowPresenter { @Override protected void onInfoClicked(ScheduleRow row) { - DvrUiHelper.startSchedulesActivity(getContext(), row.getSchedule()); + if (row.getSchedule() != null) { + DvrUiHelper.startSchedulesActivity(getContext(), row.getSchedule()); + } } @Override diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java deleted file mode 100644 index e49870f1..00000000 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui.playback; - -import android.media.tv.TvTrackInfo; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v17.leanback.app.GuidedStepFragment; -import android.support.v17.leanback.widget.GuidedAction; -import android.text.TextUtils; -import android.transition.Transition; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.android.tv.R; -import com.android.tv.util.TvSettings; - -import java.util.List; -import java.util.Locale; - -/** - * Fragment for DVR playback closed-caption/multi-audio settings. - */ -public class DvrPlaybackSideFragment extends GuidedStepFragment { - /** - * The tag for passing track infos to side fragments. - */ - public static final String TRACK_INFOS = "dvr_key_track_infos"; - /** - * The tag for passing selected track's ID to side fragments. - */ - public static final String SELECTED_TRACK_ID = "dvr_key_selected_track_id"; - - private static final int ACTION_ID_NO_SUBTITLE = -1; - private static final int CHECK_SET_ID = 1; - - private List<TvTrackInfo> mTrackInfos; - private String mSelectedTrackId; - private TvTrackInfo mSelectedTrack; - private int mTrackType; - private DvrPlaybackOverlayFragment mOverlayFragment; - - @Override - public void onCreate(Bundle savedInstanceState) { - mTrackInfos = getArguments().getParcelableArrayList(TRACK_INFOS); - mTrackType = mTrackInfos.get(0).getType(); - mSelectedTrackId = getArguments().getString(SELECTED_TRACK_ID); - mOverlayFragment = ((DvrPlaybackOverlayFragment) getFragmentManager() - .findFragmentById(R.id.dvr_playback_controls_fragment)); - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View backgroundView = super.onCreateBackgroundView(inflater, container, savedInstanceState); - backgroundView.setBackgroundColor(getResources() - .getColor(R.color.lb_playback_controls_background_light)); - return backgroundView; - } - - @Override - public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { - if (mTrackType == TvTrackInfo.TYPE_SUBTITLE) { - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_NO_SUBTITLE) - .title(getString(R.string.closed_caption_option_item_off)) - .checkSetId(CHECK_SET_ID) - .checked(mSelectedTrackId == null) - .build()); - } - for (int i = 0; i < mTrackInfos.size(); i++) { - TvTrackInfo info = mTrackInfos.get(i); - boolean checked = TextUtils.equals(info.getId(), mSelectedTrackId); - GuidedAction action = new GuidedAction.Builder(getActivity()) - .id(i) - .title(getTrackLabel(info, i)) - .checkSetId(CHECK_SET_ID) - .checked(checked) - .build(); - actions.add(action); - if (checked) { - mSelectedTrack = info; - } - } - } - - @Override - public void onGuidedActionFocused(GuidedAction action) { - int actionId = (int) action.getId(); - mOverlayFragment.selectTrack(mTrackType, actionId < 0 ? null : mTrackInfos.get(actionId)); - } - - @Override - public void onGuidedActionClicked(GuidedAction action) { - int actionId = (int) action.getId(); - mSelectedTrack = actionId < 0 ? null : mTrackInfos.get(actionId); - TvSettings.setDvrPlaybackTrackSettings(getContext(), mTrackType, mSelectedTrack); - getFragmentManager().popBackStack(); - } - - @Override - public void onStart() { - super.onStart(); - // Workaround: when overlay fragment is faded out, any focus will lost due to overlay - // fragment's implementation. So we disable overlay fragment's fading here to prevent - // losing focus while users are interacting with the side fragment. - mOverlayFragment.setFadingEnabled(false); - } - - @Override - public void onStop() { - super.onStop(); - // We disable fading of overlay fragment to prevent side fragment from losing focus, - // therefore we should resume it here. - mOverlayFragment.setFadingEnabled(true); - mOverlayFragment.selectTrack(mTrackType, mSelectedTrack); - } - - private String getTrackLabel(TvTrackInfo track, int trackIndex) { - if (track.getLanguage() != null) { - return new Locale(track.getLanguage()).getDisplayName(); - } - return track.getType() == TvTrackInfo.TYPE_SUBTITLE ? - getString(R.string.closed_caption_unknown_language, trackIndex + 1) - : getString(R.string.multi_audio_unknown_language); - } - - @Override - protected void onProvideFragmentTransitions() { - super.onProvideFragmentTransitions(); - // Excludes the background scrim from transition to prevent the blinking caused by - // hiding the overlay fragment and sliding in the side fragment at the same time. - Transition t = getEnterTransition(); - if (t != null) { - t.excludeTarget(R.id.guidedstep_background, true); - } - } -}
\ No newline at end of file diff --git a/src/com/android/tv/experiments/ExperimentFlag.java b/src/com/android/tv/experiments/ExperimentFlag.java index c0cbd643..8f60c2b5 100644 --- a/src/com/android/tv/experiments/ExperimentFlag.java +++ b/src/com/android/tv/experiments/ExperimentFlag.java @@ -16,19 +16,12 @@ package com.android.tv.experiments; -import android.support.annotation.VisibleForTesting; /** * Experiments return values based on user, device and other criteria. */ public final class ExperimentFlag<T> { - - private static boolean sAllowOverrides = false; - - @VisibleForTesting - public static void initForTest() { - sAllowOverrides = true; - } + private final T mDefaultValue; /** Returns a boolean experiment */ public static ExperimentFlag<Boolean> createFlag( @@ -37,11 +30,6 @@ public final class ExperimentFlag<T> { defaultValue); } - private final T mDefaultValue; - - private T mOverrideValue = null; - private boolean mOverridden = false; - private ExperimentFlag( T defaultValue) { mDefaultValue = defaultValue; @@ -49,22 +37,6 @@ public final class ExperimentFlag<T> { /** Returns value for this experiment */ public T get() { - return sAllowOverrides && mOverridden ? mOverrideValue : mDefaultValue; + return mDefaultValue; } - - @VisibleForTesting - public void override(T t) { - if (sAllowOverrides) { - mOverridden = true; - mOverrideValue = t; - } - } - - @VisibleForTesting - public void resetOverride() { - mOverridden = false; - } - - - } diff --git a/src/com/android/tv/experiments/Experiments.java b/src/com/android/tv/experiments/Experiments.java index e17fc300..f16c8d1e 100644 --- a/src/com/android/tv/experiments/Experiments.java +++ b/src/com/android/tv/experiments/Experiments.java @@ -23,16 +23,11 @@ import com.android.tv.common.BuildConfig; /** * Set of experiments visible in AOSP. * - * <p>This file is maintained by hand. + * <p> + * This file is maintained by hand. */ public final class Experiments { public static final ExperimentFlag<Boolean> CLOUD_EPG = createFlag( - true); - - /** - * Use network tuner if it is available and there is no other tuner types. - */ - public static final ExperimentFlag<Boolean> NETWORK_TUNER = createFlag( false); /** diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java index bec4e462..120b3dba 100644 --- a/src/com/android/tv/guide/ProgramGuide.java +++ b/src/com/android/tv/guide/ProgramGuide.java @@ -48,7 +48,7 @@ import com.android.tv.ChannelTuner; import com.android.tv.Features; import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.util.DurationTimer; +import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.Tracker; import com.android.tv.common.WeakHandler; import com.android.tv.data.ChannelDataManager; diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java index d5fb418f..4c7a4404 100644 --- a/src/com/android/tv/guide/ProgramItemView.java +++ b/src/com/android/tv/guide/ProgramItemView.java @@ -44,8 +44,8 @@ import com.android.tv.analytics.Tracker; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.guide.ProgramManager.TableEntry; import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; @@ -106,19 +106,18 @@ public class ProgramItemView extends TextView { }, entry.getWidth() > ((ProgramItemView) view).mMaxWidthForRipple ? 0 : view.getResources() .getInteger(R.integer.program_guide_ripple_anim_duration)); - } else if (entry.program != null && CommonFeatures.DVR.isEnabled(view.getContext())) { + } else if (CommonFeatures.DVR.isEnabled(view.getContext())) { DvrManager dvrManager = singletons.getDvrManager(); if (entry.entryStartUtcMillis > System.currentTimeMillis() && dvrManager.isProgramRecordable(entry.program)) { if (entry.scheduledRecording == null) { - DvrUiHelper.checkStorageStatusAndShowErrorMessage(tvActivity, - channel.getInputId(), new Runnable() { - @Override - public void run() { - DvrUiHelper.requestRecordingFutureProgram(tvActivity, - entry.program, false); - } - }); + if (DvrUiHelper.checkStorageStatusAndShowErrorMessage(tvActivity, + channel.getInputId()) + && DvrUiHelper.handleCreateSchedule(tvActivity, entry.program)) { + String msg = view.getContext().getString( + R.string.dvr_msg_program_scheduled, entry.program.getTitle()); + ToastUtils.show(view.getContext(), msg, Toast.LENGTH_SHORT); + } } else { dvrManager.removeScheduledRecording(entry.scheduledRecording); String msg = view.getResources().getString( diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java index e543fd05..e3d919df 100644 --- a/src/com/android/tv/guide/ProgramManager.java +++ b/src/com/android/tv/guide/ProgramManager.java @@ -29,7 +29,7 @@ import com.android.tv.data.ProgramDataManager; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.DvrScheduleManager.OnConflictStateChangeListener; -import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -439,24 +439,11 @@ public class ProgramManager { mChannels = mChannelDataManager.getBrowsableChannelList(); mSelectedGenreId = GenreItems.ID_ALL_CHANNELS; mFilteredChannels = mChannels; - updateTableEntriesWithoutNotification(clearPreviousTableEntries); - // Channel update notification should be called after updating table entries, so that - // the listener can get the entries. notifyChannelsUpdated(); - notifyTableEntriesUpdated(); - buildGenreFilters(); + updateTableEntries(clearPreviousTableEntries); } private void updateTableEntries(boolean clear) { - updateTableEntriesWithoutNotification(clear); - notifyTableEntriesUpdated(); - buildGenreFilters(); - } - - /** - * Updates the table entries without notifying the change. - */ - private void updateTableEntriesWithoutNotification(boolean clear) { if (clear) { mChannelIdEntriesMap.clear(); } @@ -504,6 +491,9 @@ public class ProgramManager { } } } + + notifyTableEntriesUpdated(); + buildGenreFilters(); } private void notifyGenresUpdated() { diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java index b9a0593d..e4a67972 100644 --- a/src/com/android/tv/guide/ProgramTableAdapter.java +++ b/src/com/android/tv/guide/ProgramTableAdapter.java @@ -45,21 +45,19 @@ import android.view.ViewGroup; import android.view.ViewParent; import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityManager; -import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.common.TvCommonUtils; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.data.Program.CriticScore; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.guide.ProgramManager.TableEntriesUpdatedListener; import com.android.tv.parental.ParentalControlSettings; import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; @@ -243,6 +241,16 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte notifyItemChanged(channelIndex, true); } + @Override + public void onViewAttachedToWindow(ProgramRowHolder holder) { + holder.onAttachedToWindow(); + } + + @Override + public void onViewDetachedFromWindow(ProgramRowHolder holder) { + holder.onDetachedFromWindow(); + } + // TODO: make it static public class ProgramRowHolder extends RecyclerView.ViewHolder implements ProgramRow.ChildFocusListener { @@ -304,40 +312,11 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte private final ImageView mInputLogoView; private boolean mIsInputLogoVisible; - private AccessibilityStateChangeListener mAccessibilityStateChangeListener = - new AccessibilityManager.AccessibilityStateChangeListener() { - @Override - public void onAccessibilityStateChanged(boolean enable) { - enable &= !TvCommonUtils.isRunningInTest(); - mDetailView.setFocusable(enable); - mChannelHeaderView.setFocusable(enable); - } - }; public ProgramRowHolder(View itemView) { super(itemView); mContainer = (ViewGroup) itemView; - mContainer.addOnAttachStateChangeListener( - new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - mContainer - .getViewTreeObserver() - .addOnGlobalFocusChangeListener(mGlobalFocusChangeListener); - mAccessibilityManager.addAccessibilityStateChangeListener( - mAccessibilityStateChangeListener); - } - - @Override - public void onViewDetachedFromWindow(View v) { - mContainer - .getViewTreeObserver() - .removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener); - mAccessibilityManager.removeAccessibilityStateChangeListener( - mAccessibilityStateChangeListener); - } - }); mProgramRow = (ProgramRow) mContainer.findViewById(R.id.row); mDetailView = (ViewGroup) mContainer.findViewById(R.id.detail); @@ -360,11 +339,16 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte mChannelLogoView = (ImageView) mContainer.findViewById(R.id.channel_logo); mChannelBlockView = (ImageView) mContainer.findViewById(R.id.channel_block); mInputLogoView = (ImageView) mContainer.findViewById(R.id.input_logo); - // TODO: Find a better way to handle talk back. - boolean accessibilityEnabled = mAccessibilityManager.isEnabled() - && !TvCommonUtils.isRunningInTest(); - mDetailView.setFocusable(accessibilityEnabled); - mChannelHeaderView.setFocusable(accessibilityEnabled); + mDetailView.setFocusable(mAccessibilityManager.isEnabled()); + mChannelHeaderView.setFocusable(mAccessibilityManager.isEnabled()); + mAccessibilityManager.addAccessibilityStateChangeListener( + new AccessibilityManager.AccessibilityStateChangeListener() { + @Override + public void onAccessibilityStateChanged(boolean enable) { + mDetailView.setFocusable(enable); + mChannelHeaderView.setFocusable(enable); + } + }); } public void onBind(int position) { @@ -524,6 +508,16 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte }); } + private void onAttachedToWindow() { + mContainer.getViewTreeObserver() + .addOnGlobalFocusChangeListener(mGlobalFocusChangeListener); + } + + private void onDetachedFromWindow() { + mContainer.getViewTreeObserver() + .removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener); + } + private void updateDetailView() { if (mSelectedEntry == null) { // The view holder is never on focus before. diff --git a/src/com/android/tv/guide/TimeListAdapter.java b/src/com/android/tv/guide/TimeListAdapter.java index 82907a2d..868fed46 100644 --- a/src/com/android/tv/guide/TimeListAdapter.java +++ b/src/com/android/tv/guide/TimeListAdapter.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.text.format.DateFormat; @@ -26,10 +25,8 @@ import android.view.ViewGroup; import android.widget.TextView; import com.android.tv.R; -import com.android.tv.util.Utils; import java.util.Date; -import java.util.Locale; import java.util.concurrent.TimeUnit; /** @@ -38,28 +35,16 @@ import java.util.concurrent.TimeUnit; */ public class TimeListAdapter extends RecyclerView.Adapter<TimeListAdapter.TimeViewHolder> { private static final long TIME_UNIT_MS = TimeUnit.MINUTES.toMillis(30); - - // Ex. 3:00 AM - private static final String TIME_PATTERN_SAME_DAY = "h:mm a"; - // Ex. Oct 21, 3:00 AM - private static final String TIME_PATTERN_DIFFERENT_DAY = "MMM d, h:mm a"; - private static int sRowHeaderOverlapping; // Nearest half hour at or before the start time. private long mStartUtcMs; - private final String mTimePatternSameDay; - private final String mTimePatternDifferentDay; public TimeListAdapter(Resources res) { if (sRowHeaderOverlapping == 0) { sRowHeaderOverlapping = Math.abs(res.getDimensionPixelOffset( R.dimen.program_guide_table_header_row_overlap)); } - Locale locale = res.getConfiguration().locale; - mTimePatternSameDay = DateFormat.getBestDateTimePattern(locale, TIME_PATTERN_SAME_DAY); - mTimePatternDifferentDay = - DateFormat.getBestDateTimePattern(locale, TIME_PATTERN_DIFFERENT_DAY); } public void update(long startTimeMs) { @@ -83,14 +68,10 @@ public class TimeListAdapter extends RecyclerView.Adapter<TimeListAdapter.TimeVi long endTime = startTime + TIME_UNIT_MS; View itemView = holder.itemView; - Date timeDate = new Date(startTime); - String timeString; - if (Utils.isInGivenDay(System.currentTimeMillis(), startTime)) { - timeString = DateFormat.format(mTimePatternSameDay, timeDate).toString(); - } else { - timeString = DateFormat.format(mTimePatternDifferentDay, timeDate).toString(); - } - ((TextView) itemView.findViewById(R.id.time)).setText(timeString); + + TextView textView = (TextView) itemView.findViewById(R.id.time); + String time = DateFormat.getTimeFormat(itemView.getContext()).format(new Date(startTime)); + textView.setText(time); RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) itemView.getLayoutParams(); lp.width = GuideUtils.convertMillisToPixel(startTime, endTime); diff --git a/src/com/android/tv/menu/ActionCardView.java b/src/com/android/tv/menu/ActionCardView.java index 2fd70bfb..54892cac 100644 --- a/src/com/android/tv/menu/ActionCardView.java +++ b/src/com/android/tv/menu/ActionCardView.java @@ -19,8 +19,8 @@ package com.android.tv.menu; import android.content.Context; import android.util.AttributeSet; import android.util.Log; +import android.widget.FrameLayout; import android.widget.ImageView; -import android.widget.RelativeLayout; import android.widget.TextView; import com.android.tv.R; @@ -28,7 +28,7 @@ import com.android.tv.R; /** * A view to render an item of TV options. */ -public class ActionCardView extends RelativeLayout implements ItemListRowView.CardView<MenuAction> { +public class ActionCardView extends FrameLayout implements ItemListRowView.CardView<MenuAction> { private static final String TAG = MenuView.TAG; private static final boolean DEBUG = MenuView.DEBUG; @@ -66,7 +66,7 @@ public class ActionCardView extends RelativeLayout implements ItemListRowView.Ca } mIconView.setImageDrawable(action.getDrawable(getContext())); mLabelView.setText(action.getActionName(getContext())); - mStateView.setText(action.getActionDescription()); + mStateView.setText(action.getActionDescription(getContext())); if (action.isEnabled()) { setEnabled(true); setFocusable(true); diff --git a/src/com/android/tv/menu/AppLinkCardView.java b/src/com/android/tv/menu/AppLinkCardView.java index d23d9a00..bfb5e3f1 100644 --- a/src/com/android/tv/menu/AppLinkCardView.java +++ b/src/com/android/tv/menu/AppLinkCardView.java @@ -24,7 +24,6 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.os.AsyncTask; import android.support.annotation.Nullable; import android.support.v7.graphics.Palette; import android.text.TextUtils; @@ -56,6 +55,7 @@ public class AppLinkCardView extends BaseCardView<Channel> { private final int mIconColorFilter; private ImageView mImageView; + private View mGradientView; private TextView mAppInfoView; private View mMetaViewHolder; private Channel mChannel; @@ -102,115 +102,35 @@ public class AppLinkCardView extends BaseCardView<Channel> { int linkType = mChannel.getAppLinkType(getContext()); mIntent = mChannel.getAppLinkIntent(getContext()); - CharSequence appLabel = null; - mImageView.setForeground(null); switch (linkType) { case Channel.APP_LINK_TYPE_CHANNEL: setText(mChannel.getAppLinkText()); mAppInfoView.setVisibility(VISIBLE); + mGradientView.setVisibility(VISIBLE); mAppInfoView.setCompoundDrawablePadding(mIconPadding); - mAppInfoView.setCompoundDrawablesRelative(null, null, null, null); - appLabel = mTvInputManagerHelper.getTvInputApplicationLabel(channel.getInputId()); - if (appLabel != null) { - mAppInfoView.setText(appLabel); - } else { - new AsyncTask<Void, Void, CharSequence>() { - private final String mLoadTvInputId = mChannel.getInputId(); - - @Override - protected CharSequence doInBackground(Void... params) { - if (appInfo != null) { - return mPackageManager.getApplicationLabel(appInfo); - } - return null; - } - - @Override - protected void onPostExecute(CharSequence appLabel) { - mTvInputManagerHelper.setTvInputApplicationLabel( - mLoadTvInputId, appLabel); - if (mLoadTvInputId != mChannel.getInputId() || !isAttachedToWindow()) { - return; - } - mAppInfoView.setText(appLabel); - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } + mAppInfoView.setCompoundDrawables(null, null, null, null); + mAppInfoView.setText(mPackageManager.getApplicationLabel(appInfo)); if (!TextUtils.isEmpty(mChannel.getAppLinkIconUri())) { mChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON, - mIconWidth, mIconHeight, - createChannelLogoCallback( - this, mChannel, Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON)); + mIconWidth, mIconHeight, createChannelLogoCallback(this, mChannel, + Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON)); } else if (appInfo.icon != 0) { - Drawable appIcon = - mTvInputManagerHelper.getTvInputApplicationIcon(mChannel.getInputId()); - if (appIcon != null) { - BitmapUtils.setColorFilterToDrawable(mIconColorFilter, appIcon); - appIcon.setBounds(0, 0, mIconWidth, mIconHeight); - mAppInfoView.setCompoundDrawablesRelative(appIcon, null, null, null); - } else { - new AsyncTask<Void, Void, Drawable>() { - private final String mLoadTvInputId = mChannel.getInputId(); - - @Override - protected Drawable doInBackground(Void... params) { - if (appInfo != null) { - return mPackageManager.getApplicationIcon(appInfo); - } - return null; - } - - @Override - protected void onPostExecute(Drawable appIcon) { - mTvInputManagerHelper.setTvInputApplicationIcon( - mLoadTvInputId, appIcon); - if (mLoadTvInputId != mChannel.getInputId() - || !isAttachedToWindow()) { - return; - } - BitmapUtils.setColorFilterToDrawable(mIconColorFilter, appIcon); - appIcon.setBounds(0, 0, mIconWidth, mIconHeight); - mAppInfoView.setCompoundDrawablesRelative(appIcon, null, null, null); - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } + Drawable appIcon = mPackageManager.getApplicationIcon(appInfo); + BitmapUtils.setColorFilterToDrawable(mIconColorFilter, appIcon); + appIcon.setBounds(0, 0, mIconWidth, mIconHeight); + mAppInfoView.setCompoundDrawables(appIcon, null, null, null); } break; case Channel.APP_LINK_TYPE_APP: - appLabel = mTvInputManagerHelper.getTvInputApplicationLabel(mChannel.getInputId()); - if (appLabel != null) { - setText(getContext() - .getString(R.string.channels_item_app_link_app_launcher, appLabel)); - } else { - new AsyncTask<Void, Void, CharSequence>() { - private final String mLoadTvInputId = mChannel.getInputId(); - - @Override - protected CharSequence doInBackground(Void... params) { - if (appInfo != null) { - return mPackageManager.getApplicationLabel(appInfo); - } - return null; - } - - @Override - protected void onPostExecute(CharSequence appLabel) { - mTvInputManagerHelper.setTvInputApplicationLabel( - mLoadTvInputId, appLabel); - if (mLoadTvInputId != mChannel.getInputId() || !isAttachedToWindow()) { - return; - } - setText(getContext() - .getString( - R.string.channels_item_app_link_app_launcher, - appLabel)); - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } + setText(getContext().getString( + R.string.channels_item_app_link_app_launcher, + mPackageManager.getApplicationLabel(appInfo))); mAppInfoView.setVisibility(GONE); + mGradientView.setVisibility(GONE); break; default: mAppInfoView.setVisibility(GONE); + mGradientView.setVisibility(GONE); Log.d(TAG, "Should not be here."); } @@ -228,6 +148,8 @@ public class AppLinkCardView extends BaseCardView<Channel> { } else { setCardImageWithBanner(appInfo); } + // Call super.onBind() at the end intentionally. In order to correctly handle extension of + // text view, text should be set before calling super.onBind. super.onBind(channel, selected); } @@ -260,14 +182,13 @@ public class AppLinkCardView extends BaseCardView<Channel> { } } BitmapUtils.setColorFilterToDrawable(mIconColorFilter, drawable); - mAppInfoView.setCompoundDrawablesRelative(drawable, null, null, null); + mAppInfoView.setCompoundDrawables(drawable, null, null, null); } else if (type == Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART) { if (bitmap == null) { setCardImageWithBanner( mTvInputManagerHelper.getTvInputAppInfo(mChannel.getInputId())); } else { mImageView.setImageBitmap(bitmap); - mImageView.setForeground(getContext().getDrawable(R.drawable.card_image_gradient)); if (mChannel.getAppLinkColor() == 0) { extractAndSetMetaViewBackgroundColor(bitmap); } @@ -279,6 +200,7 @@ public class AppLinkCardView extends BaseCardView<Channel> { protected void onFinishInflate() { super.onFinishInflate(); mImageView = (ImageView) findViewById(R.id.image); + mGradientView = findViewById(R.id.image_gradient); mAppInfoView = (TextView) findViewById(R.id.app_info); mMetaViewHolder = findViewById(R.id.app_link_text_holder); } @@ -287,85 +209,37 @@ public class AppLinkCardView extends BaseCardView<Channel> { // 1) Provided poster art image, 2) Activity banner, 3) Activity icon, 4) Application banner, // 5) Application icon, and 6) default image. private void setCardImageWithBanner(ApplicationInfo appInfo) { - new AsyncTask<Void, Void, Drawable>() { - private String mLoadTvInputId = mChannel.getInputId(); - @Override - protected Drawable doInBackground(Void... params) { - Drawable banner = null; - if (mIntent != null) { - try { - banner = mPackageManager.getActivityBanner(mIntent); - if (banner == null) { - banner = mPackageManager.getActivityIcon(mIntent); - } - } catch (PackageManager.NameNotFoundException e) { - // do nothing. - } + Drawable banner = null; + if (mIntent != null) { + try { + banner = mPackageManager.getActivityBanner(mIntent); + if (banner == null) { + banner = mPackageManager.getActivityIcon(mIntent); } - return banner; + } catch (PackageManager.NameNotFoundException e) { + // do nothing. } + } - @Override - protected void onPostExecute(Drawable banner) { - if (mLoadTvInputId != mChannel.getInputId() || !isAttachedToWindow()) { - return; - } - if (banner != null) { - setCardImageWithBannerInternal(banner); - } else { - setCardImageWithApplicationInfoBanner(appInfo); - } + if (banner == null && appInfo != null) { + if (appInfo.banner != 0) { + banner = mPackageManager.getApplicationBanner(appInfo); + } + if (banner == null && appInfo.icon != 0) { + banner = mPackageManager.getApplicationIcon(appInfo); } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - private void setCardImageWithApplicationInfoBanner(ApplicationInfo appInfo) { - Drawable appBanner = - mTvInputManagerHelper.getTvInputApplicationBanner(mChannel.getInputId()); - if (appBanner != null) { - setCardImageWithBannerInternal(appBanner); - } else { - new AsyncTask<Void, Void, Drawable>() { - private final String mLoadTvInputId = mChannel.getInputId(); - @Override - protected Drawable doInBackground(Void... params) { - Drawable banner = null; - if (appInfo != null) { - if (appInfo.banner != 0) { - banner = mPackageManager.getApplicationBanner(appInfo); - } - if (banner == null && appInfo.icon != 0) { - banner = mPackageManager.getApplicationIcon(appInfo); - } - } - return banner; - } - - @Override - protected void onPostExecute(Drawable banner) { - mTvInputManagerHelper.setTvInputApplicationBanner( - mLoadTvInputId, banner); - if (mLoadTvInputId != mChannel.getInputId() || !isAttachedToWindow()) { - return; - } - setCardImageWithBannerInternal(banner); - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - } - private void setCardImageWithBannerInternal(Drawable banner) { if (banner == null) { mImageView.setImageResource(R.drawable.ic_recent_thumbnail_default); mImageView.setBackgroundResource(R.color.channel_card); } else { - Bitmap bitmap = Bitmap.createBitmap( - mCardImageWidth, mCardImageHeight, Bitmap.Config.ARGB_8888); + Bitmap bitmap = + Bitmap.createBitmap(mCardImageWidth, mCardImageHeight, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); banner.setBounds(0, 0, mCardImageWidth, mCardImageHeight); banner.draw(canvas); mImageView.setImageDrawable(banner); - mImageView.setForeground(getContext().getDrawable(R.drawable.card_image_gradient)); if (mChannel.getAppLinkColor() == 0) { extractAndSetMetaViewBackgroundColor(bitmap); } diff --git a/src/com/android/tv/menu/BaseCardView.java b/src/com/android/tv/menu/BaseCardView.java index fa74ce3e..c6a34a5d 100644 --- a/src/com/android/tv/menu/BaseCardView.java +++ b/src/com/android/tv/menu/BaseCardView.java @@ -57,7 +57,6 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo private TextView mTextViewFocused; private final int mCardImageWidth; private final float mCardHeight; - private boolean mSelected; public BaseCardView(Context context) { this(context, null); @@ -104,9 +103,23 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo /** * Called when the view is displayed. + * + * Before onBind is called, this view's text should be set to determine if it'll be extended + * or not in focus state. */ @Override public void onBind(T item, boolean selected) { + if (mTextView != null && mTextViewFocused != null) { + mTextViewFocused.measure( + MeasureSpec.makeMeasureSpec(mCardImageWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mExtendViewOnFocus = mTextViewFocused.getLineCount() > 1; + if (mExtendViewOnFocus) { + setTextViewFocusedAlpha(selected ? 1f : 0f); + } else { + setTextViewFocusedAlpha(1f); + } + } setFocusAnimatedValue(selected ? SCALE_FACTOR_1F : SCALE_FACTOR_0F); } @@ -115,7 +128,6 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo @Override public void onSelected() { - mSelected = true; if (isAttachedToWindow() && getVisibility() == View.VISIBLE) { startFocusAnimation(SCALE_FACTOR_1F); } else { @@ -126,7 +138,6 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo @Override public void onDeselected() { - mSelected = false; if (isAttachedToWindow() && getVisibility() == View.VISIBLE) { startFocusAnimation(SCALE_FACTOR_0F); } else { @@ -145,7 +156,6 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo if (mTextView != null) { mTextView.setText(resId); } - onTextViewUpdated(); } /** @@ -158,22 +168,6 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo if (mTextView != null) { mTextView.setText(text); } - onTextViewUpdated(); - } - - private void onTextViewUpdated() { - if (mTextView != null && mTextViewFocused != null) { - mTextViewFocused.measure( - MeasureSpec.makeMeasureSpec(mCardImageWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - mExtendViewOnFocus = mTextViewFocused.getLineCount() > 1; - if (mExtendViewOnFocus) { - setTextViewFocusedAlpha(mSelected ? 1f : 0f); - } else { - setTextViewFocusedAlpha(1f); - } - } - setFocusAnimatedValue(mSelected ? SCALE_FACTOR_1F : SCALE_FACTOR_0F); } /** @@ -215,18 +209,12 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo setScaleX(scale); setScaleY(scale); setTranslationZ(mFocusTranslationZ * animatedValue); - if (mTextView != null && mTextViewFocused != null) { + if (mExtendViewOnFocus) { ViewGroup.LayoutParams params = mTextView.getLayoutParams(); - int height = mExtendViewOnFocus ? Math.round(mTextViewHeight - + (mExtendedTextViewHeight - mTextViewHeight) * animatedValue) - : (int) mTextViewHeight; - if (height != params.height) { - params.height = height; - setTextViewLayoutParams(params); - } - if (mExtendViewOnFocus) { - setTextViewFocusedAlpha(animatedValue); - } + params.height = Math.round(mTextViewHeight + + (mExtendedTextViewHeight - mTextViewHeight) * animatedValue); + setTextViewLayoutParams(params); + setTextViewFocusedAlpha(animatedValue); } } diff --git a/src/com/android/tv/menu/ChannelCardView.java b/src/com/android/tv/menu/ChannelCardView.java index 4ee56892..1c8015a6 100644 --- a/src/com/android/tv/menu/ChannelCardView.java +++ b/src/com/android/tv/menu/ChannelCardView.java @@ -45,6 +45,7 @@ public class ChannelCardView extends BaseCardView<Channel> { private final int mCardImageHeight; private ImageView mImageView; + private View mGradientView; private TextView mChannelNumberNameView; private ProgressBar mProgressBar; private Channel mChannel; @@ -70,6 +71,7 @@ public class ChannelCardView extends BaseCardView<Channel> { protected void onFinishInflate() { super.onFinishInflate(); mImageView = (ImageView) findViewById(R.id.image); + mGradientView = findViewById(R.id.image_gradient); mChannelNumberNameView = (TextView) findViewById(R.id.channel_number_and_name); mProgressBar = (ProgressBar) findViewById(R.id.progress); } @@ -86,7 +88,7 @@ public class ChannelCardView extends BaseCardView<Channel> { mChannelNumberNameView.setVisibility(VISIBLE); mImageView.setImageResource(R.drawable.ic_recent_thumbnail_default); mImageView.setBackgroundResource(R.color.channel_card); - mImageView.setForeground(null); + mGradientView.setVisibility(View.GONE); mProgressBar.setVisibility(GONE); setTextViewEnabled(true); @@ -99,6 +101,8 @@ public class ChannelCardView extends BaseCardView<Channel> { } updateProgramInformation(); + // Call super.onBind() at the end intentionally. In order to correctly handle extension of + // text view, text should be set before calling super.onBind. super.onBind(channel, selected); } @@ -119,7 +123,7 @@ public class ChannelCardView extends BaseCardView<Channel> { private void updatePosterArt(Bitmap posterArt) { mImageView.setImageBitmap(posterArt); - mImageView.setForeground(getContext().getDrawable(R.drawable.card_image_gradient)); + mGradientView.setVisibility(View.VISIBLE); } private void updateProgramInformation() { diff --git a/src/com/android/tv/menu/ChannelsRow.java b/src/com/android/tv/menu/ChannelsRow.java index 490d73de..dedf0993 100644 --- a/src/com/android/tv/menu/ChannelsRow.java +++ b/src/com/android/tv/menu/ChannelsRow.java @@ -26,14 +26,8 @@ import com.android.tv.recommendation.Recommender; public class ChannelsRow extends ItemListRow { public static final String ID = ChannelsRow.class.getName(); - /** - * Minimum count for recent channels. - */ - public static final int MIN_COUNT_FOR_RECENT_CHANNELS = 5; - /** - * Maximum count for recent channels. - */ - public static final int MAX_COUNT_FOR_RECENT_CHANNELS = 10; + private static final int MIN_COUNT_FOR_RECENT_CHANNELS = 5; + private static final int MAX_COUNT_FOR_RECENT_CHANNELS = 10; private Recommender mTvRecommendation; private ChannelsRowAdapter mChannelsAdapter; diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java index 4ba6a93a..c8e1bd05 100644 --- a/src/com/android/tv/menu/ChannelsRowAdapter.java +++ b/src/com/android/tv/menu/ChannelsRowAdapter.java @@ -31,6 +31,7 @@ import com.android.tv.dvr.DvrDataManager; 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; @@ -129,6 +130,8 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel> @Override public void onBindViewHolder(MyViewHolder viewHolder, int position) { + super.onBindViewHolder(viewHolder, position); + int viewType = getItemViewType(position); if (viewType == R.layout.menu_card_guide) { viewHolder.itemView.setOnClickListener(mGuideOnClickListener); @@ -144,7 +147,6 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel> viewHolder.itemView.setTag(getItemList().get(position)); viewHolder.itemView.setOnClickListener(mChannelOnClickListener); } - super.onBindViewHolder(viewHolder, position); } @Override diff --git a/src/com/android/tv/menu/ItemListRowView.java b/src/com/android/tv/menu/ItemListRowView.java index 01257628..4919c595 100644 --- a/src/com/android/tv/menu/ItemListRowView.java +++ b/src/com/android/tv/menu/ItemListRowView.java @@ -28,7 +28,6 @@ import android.view.ViewGroup; import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.util.ViewCache; import java.util.Collections; import java.util.List; @@ -195,20 +194,9 @@ public class ItemListRowView extends MenuRowView implements OnChildSelectedListe return mItemList.size(); } - /** - * Returns the position of the item. - */ - protected int getItemPosition(T item) { - return mItemList.indexOf(item); - } - @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - int resId = getLayoutResId(viewType); - View view = ViewCache.getInstance().getView(resId); - if (view == null) { - view = mLayoutInflater.inflate(resId, parent, false); - } + View view = mLayoutInflater.inflate(getLayoutResId(viewType), parent, false); return new MyViewHolder(view); } diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java index 25e629c1..1160a5b5 100644 --- a/src/com/android/tv/menu/Menu.java +++ b/src/com/android/tv/menu/Menu.java @@ -27,30 +27,23 @@ import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; import com.android.tv.ChannelTuner; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.TvOptionsManager; +import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.Tracker; import com.android.tv.common.TvCommonUtils; import com.android.tv.common.WeakHandler; import com.android.tv.menu.MenuRowFactory.PartnerRow; +import com.android.tv.menu.MenuRowFactory.PipOptionsRow; import com.android.tv.menu.MenuRowFactory.TvOptionsRow; import com.android.tv.ui.TunableTvView; -import com.android.tv.util.DurationTimer; -import com.android.tv.util.ViewCache; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; /** * A class which controls the menu. @@ -88,21 +81,10 @@ public class Menu { sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_JUMP_TO_NEXT } - private static final Map<Integer, Integer> PRELOAD_VIEW_IDS = new HashMap<>(); - static { - PRELOAD_VIEW_IDS.put(R.layout.menu_card_guide, 1); - PRELOAD_VIEW_IDS.put(R.layout.menu_card_setup, 1); - PRELOAD_VIEW_IDS.put(R.layout.menu_card_dvr, 1); - PRELOAD_VIEW_IDS.put(R.layout.menu_card_app_link, 1); - PRELOAD_VIEW_IDS.put(R.layout.menu_card_channel, ChannelsRow.MAX_COUNT_FOR_RECENT_CHANNELS); - PRELOAD_VIEW_IDS.put(R.layout.menu_card_action, 7); - } - private static final String SCREEN_NAME = "Menu"; private static final int MSG_HIDE_MENU = 1000; - private final Context mContext; private final IMenuView mMenuView; private final Tracker mTracker; private final DurationTimer mVisibleTimer = new DurationTimer(); @@ -121,16 +103,15 @@ public class Menu { @VisibleForTesting Menu(Context context, IMenuView menuView, MenuRowFactory menuRowFactory, OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) { - this(context, null, null, menuView, menuRowFactory, onMenuVisibilityChangeListener); + this(context, null, menuView, menuRowFactory, onMenuVisibilityChangeListener); } - public Menu(Context context, TunableTvView tvView, TvOptionsManager optionsManager, - IMenuView menuView, MenuRowFactory menuRowFactory, + public Menu(Context context, TunableTvView tvView, IMenuView menuView, + MenuRowFactory menuRowFactory, OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) { - mContext = context; mMenuView = menuView; mTracker = TvApplication.getSingletons(context).getTracker(); - mMenuUpdater = new MenuUpdater(this, tvView, optionsManager); + mMenuUpdater = new MenuUpdater(context, tvView, this); Resources res = context.getResources(); mShowDurationMillis = res.getInteger(R.integer.menu_show_duration); mOnMenuVisibilityChangeListener = onMenuVisibilityChangeListener; @@ -149,6 +130,7 @@ public class Menu { addMenuRow(menuRowFactory.createMenuRow(this, ChannelsRow.class)); addMenuRow(menuRowFactory.createMenuRow(this, PartnerRow.class)); addMenuRow(menuRowFactory.createMenuRow(this, TvOptionsRow.class)); + addMenuRow(menuRowFactory.createMenuRow(this, PipOptionsRow.class)); mMenuView.setMenuRows(mMenuRows); } @@ -178,23 +160,6 @@ public class Menu { } /** - * Preloads the item view used for the menu. - */ - public void preloadItemViews() { - LayoutInflater inflater = - (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - // Use a fake parent to make the layoutParams set correctly. - ViewGroup fakeParent = new LinearLayout(mContext); - for (int id : PRELOAD_VIEW_IDS.keySet()) { - int count = PRELOAD_VIEW_IDS.get(id); - for (int i = 0; i < count; i++) { - View view = inflater.inflate(id, fakeParent, false); - ViewCache.getInstance().putView(id, view); - } - } - } - - /** * Shows the main menu. * * @param reason A reason why this is called. See {@link MenuShowReason} diff --git a/src/com/android/tv/menu/MenuAction.java b/src/com/android/tv/menu/MenuAction.java index b4356059..0d59552a 100644 --- a/src/com/android/tv/menu/MenuAction.java +++ b/src/com/android/tv/menu/MenuAction.java @@ -20,9 +20,9 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.text.TextUtils; +import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvOptionsManager; -import com.android.tv.TvOptionsManager.OptionType; /** * A class to define possible actions from main menu. @@ -36,9 +36,12 @@ 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_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); + R.drawable.ic_pip_option_layout2); public static final MenuAction SELECT_AUDIO_LANGUAGE_ACTION = new MenuAction(R.string.options_item_multi_audio, TvOptionsManager.OPTION_MULTI_AUDIO, R.drawable.ic_tvoption_multi_track); @@ -48,36 +51,34 @@ public class MenuAction { public static final MenuAction DEV_ACTION = new MenuAction(R.string.options_item_developer, TvOptionsManager.OPTION_DEVELOPER, R.drawable.ic_developer_mode_tv_white_48dp); + // TODO: Change the icon. public static final MenuAction SETTINGS_ACTION = new MenuAction(R.string.options_item_settings, TvOptionsManager.OPTION_SETTINGS, R.drawable.ic_settings); + // Actions in the PIP option row. + public static final MenuAction PIP_SELECT_INPUT_ACTION = + new MenuAction(R.string.pip_options_item_source, TvOptionsManager.OPTION_PIP_INPUT, + R.drawable.ic_pip_option_input); + public static final MenuAction PIP_SWAP_ACTION = + new MenuAction(R.string.pip_options_item_swap, TvOptionsManager.OPTION_PIP_SWAP, + R.drawable.ic_pip_option_swap); + public static final MenuAction PIP_SOUND_ACTION = + new MenuAction(R.string.pip_options_item_sound, TvOptionsManager.OPTION_PIP_SOUND, + R.drawable.ic_pip_option_swap_audio); + public static final MenuAction PIP_LAYOUT_ACTION = + new MenuAction(R.string.pip_options_item_layout, TvOptionsManager.OPTION_PIP_LAYOUT, + R.drawable.ic_pip_option_layout1); + public static final MenuAction PIP_SIZE_ACTION = + new MenuAction(R.string.pip_options_item_size, TvOptionsManager.OPTION_PIP_SIZE, + R.drawable.ic_pip_option_size); private final String mActionName; private final int mActionNameResId; - @OptionType private final int mType; - private String mActionDescription; + private final int mType; private Drawable mDrawable; private int mDrawableResId; private boolean mEnabled = true; - /** - * Sets the action description. Returns {@code trye} if the description is changed. - */ - public static boolean setActionDescription(MenuAction action, String actionDescription) { - String oldDescription = action.mActionDescription; - action.mActionDescription = actionDescription; - return !TextUtils.equals(action.mActionDescription, oldDescription); - } - - /** - * Enables or disables the action. Returns {@code true} if the value is changed. - */ - public static boolean setEnabled(MenuAction action, boolean enabled) { - boolean changed = action.mEnabled != enabled; - action.mEnabled = enabled; - return changed; - } - public MenuAction(int actionNameResId, int type, int drawableResId) { mActionName = null; mActionNameResId = actionNameResId; @@ -101,11 +102,11 @@ public class MenuAction { return context.getString(mActionNameResId); } - public String getActionDescription() { - return mActionDescription; + public String getActionDescription(Context context) { + return ((MainActivity) context).getTvOptionsManager().getOptionString(mType); } - @OptionType public int getType() { + public int getType() { return mType; } @@ -119,10 +120,28 @@ public class MenuAction { return mDrawable; } + /** + * Sets drawable resource id. + * + * @return {@code true} if drawable is changed. + */ + public boolean setDrawableResId(int resId) { + if (mDrawableResId == resId) { + return false; + } + mDrawable = null; + mDrawableResId = resId; + return true; + } + public boolean isEnabled() { return mEnabled; } + public void setEnabled(boolean enabled) { + mEnabled = enabled; + } + public int getActionNameResId() { return mActionNameResId; } diff --git a/src/com/android/tv/menu/MenuLayoutManager.java b/src/com/android/tv/menu/MenuLayoutManager.java index a16ac197..6c767247 100644 --- a/src/com/android/tv/menu/MenuLayoutManager.java +++ b/src/com/android/tv/menu/MenuLayoutManager.java @@ -384,15 +384,10 @@ public class MenuLayoutManager { mSelectedPosition = position; if (DEBUG) dumpChildren("startRowAnimation()"); + MenuRowView currentView = mMenuRowViews.get(position); // Show the children of the next row. - final MenuRowView currentView = mMenuRowViews.get(position); - TextView currentTitleView = currentView.getTitleView(); - View currentContentsView = currentView.getContentsView(); - currentTitleView.setVisibility(View.VISIBLE); - currentContentsView.setVisibility(View.VISIBLE); - if (currentView instanceof PlayControlsRowView) { - ((PlayControlsRowView) currentView).onPreselected(); - } + currentView.getTitleView().setVisibility(View.VISIBLE); + currentView.getContentsView().setVisibility(View.VISIBLE); // Request focus after the new contents view shows up. mMenuView.requestFocus(); if (mTempTitleViewForOld == null) { @@ -412,7 +407,7 @@ public class MenuLayoutManager { // Old row. MenuRow oldRow = mMenuRows.get(oldPosition); - final MenuRowView oldView = mMenuRowViews.get(oldPosition); + MenuRowView oldView = mMenuRowViews.get(oldPosition); View oldContentsView = oldView.getContentsView(); // Old contents view. animators.add(createAlphaAnimator(oldContentsView, 1.0f, 0.0f, 1.0f, mLinearOutSlowIn) @@ -473,6 +468,8 @@ public class MenuLayoutManager { } // Current row. Rect currentLayoutRect = new Rect(layouts.get(position)); + TextView currentTitleView = currentView.getTitleView(); + View currentContentsView = currentView.getContentsView(); currentContentsView.setAlpha(0.0f); if (scrollDown) { // Current title view. @@ -575,8 +572,9 @@ public class MenuLayoutManager { for (ViewPropertyValueHolder holder : propertyValuesAfterAnimation) { holder.property.set(holder.view, holder.value); } - oldView.onDeselected(); - currentView.onSelected(true); + oldTitleView.setVisibility(View.VISIBLE); + mMenuRowViews.get(oldPosition).onDeselected(); + mMenuRowViews.get(position).onSelected(true); mTempTitleViewForOld.setVisibility(View.GONE); mTempTitleViewForCurrent.setVisibility(View.GONE); layout(mMenuView.getLeft(), mMenuView.getTop(), mMenuView.getRight(), diff --git a/src/com/android/tv/menu/MenuRowFactory.java b/src/com/android/tv/menu/MenuRowFactory.java index 2d5453fe..c67a0e04 100644 --- a/src/com/android/tv/menu/MenuRowFactory.java +++ b/src/com/android/tv/menu/MenuRowFactory.java @@ -67,6 +67,8 @@ public class MenuRowFactory { } else if (TvOptionsRow.class.equals(key)) { return new TvOptionsRow(mMainActivity, menu, mTvCustomizationManager .getCustomActions(TvCustomizationManager.ID_OPTIONS_ROW)); + } else if (PipOptionsRow.class.equals(key)) { + return new PipOptionsRow(mMainActivity, menu); } return null; } @@ -75,9 +77,6 @@ public class MenuRowFactory { * A menu row which represents the TV options row. */ public static class TvOptionsRow extends ItemListRow { - /** The ID of the row. */ - public static final String ID = TvOptionsRow.class.getName(); - private TvOptionsRow(Context context, Menu menu, List<CustomAction> customActions) { super(context, menu, R.string.menu_title_options, R.dimen.action_card_height, new TvOptionsRowAdapter(context, customActions)); @@ -92,6 +91,25 @@ public class MenuRowFactory { } /** + * A menu row which represents the PIP options row. + */ + public static class PipOptionsRow extends ItemListRow { + private final MainActivity mMainActivity; + + private PipOptionsRow(Context context, Menu menu) { + super(context, menu, R.string.menu_title_pip_options, R.dimen.action_card_height, + new PipOptionsRowAdapter(context)); + mMainActivity = (MainActivity) context; + } + + @Override + public boolean isVisible() { + // TODO: Remove the dependency on MainActivity. + return super.isVisible() && mMainActivity.isPipEnabled(); + } + } + + /** * A menu row which represents the partner row. */ public static class PartnerRow extends ItemListRow { diff --git a/src/com/android/tv/menu/MenuUpdater.java b/src/com/android/tv/menu/MenuUpdater.java index 7ad38e74..075b299e 100644 --- a/src/com/android/tv/menu/MenuUpdater.java +++ b/src/com/android/tv/menu/MenuUpdater.java @@ -16,14 +16,11 @@ package com.android.tv.menu; +import android.content.Context; import android.support.annotation.Nullable; import com.android.tv.ChannelTuner; -import com.android.tv.TvOptionsManager; -import com.android.tv.TvOptionsManager.OptionChangedListener; -import com.android.tv.TvOptionsManager.OptionType; import com.android.tv.data.Channel; -import com.android.tv.menu.MenuRowFactory.TvOptionsRow; import com.android.tv.ui.TunableTvView; import com.android.tv.ui.TunableTvView.OnScreenBlockingChangedListener; @@ -33,10 +30,10 @@ import com.android.tv.ui.TunableTvView.OnScreenBlockingChangedListener; * <p>As the menu is updated when it shows up, this class handles only the dynamic updates. */ public class MenuUpdater { - private final Menu mMenu; // Can be null for testing. - @Nullable private final TunableTvView mTvView; - @Nullable private final TvOptionsManager mOptionsManager; + @Nullable + private final TunableTvView mTvView; + private final Menu mMenu; private ChannelTuner mChannelTuner; private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() { @@ -45,7 +42,7 @@ public class MenuUpdater { @Override public void onBrowsableChannelListChanged() { - mMenu.update(ChannelsRow.ID); + mMenu.update(); } @Override @@ -56,17 +53,10 @@ public class MenuUpdater { mMenu.update(ChannelsRow.ID); } }; - private final OptionChangedListener mOptionChangeListener = new OptionChangedListener() { - @Override - public void onOptionChanged(@OptionType int optionType, String newString) { - mMenu.update(TvOptionsRow.ID); - } - }; - public MenuUpdater(Menu menu, TunableTvView tvView, TvOptionsManager optionsManager) { - mMenu = menu; + public MenuUpdater(Context context, TunableTvView tvView, Menu menu) { mTvView = tvView; - mOptionsManager = optionsManager; + mMenu = menu; if (mTvView != null) { mTvView.setOnScreenBlockedListener(new OnScreenBlockingChangedListener() { @Override @@ -75,18 +65,11 @@ public class MenuUpdater { } }); } - if (mOptionsManager != null) { - mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_CLOSED_CAPTIONS, - mOptionChangeListener); - mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_DISPLAY_MODE, - mOptionChangeListener); - mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_MULTI_AUDIO, - mOptionChangeListener); - } } /** - * Sets the instance of {@link ChannelTuner}. Call this method when the channel tuner is ready. + * Sets the instance of {@link ChannelTuner}. Call this method when the channel tuner is ready + * or not available any more. */ public void setChannelTuner(ChannelTuner channelTuner) { if (mChannelTuner != null) { @@ -96,6 +79,7 @@ public class MenuUpdater { if (mChannelTuner != null) { mChannelTuner.addListener(mChannelTunerListener); } + mMenu.update(); } /** @@ -108,10 +92,5 @@ public class MenuUpdater { if (mTvView != null) { mTvView.setOnScreenBlockedListener(null); } - if (mOptionsManager != null) { - mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_CLOSED_CAPTIONS, null); - mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_DISPLAY_MODE, null); - mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_MULTI_AUDIO, null); - } } } diff --git a/src/com/android/tv/menu/OptionsRowAdapter.java b/src/com/android/tv/menu/OptionsRowAdapter.java index dd6194a1..93bd0a4d 100644 --- a/src/com/android/tv/menu/OptionsRowAdapter.java +++ b/src/com/android/tv/menu/OptionsRowAdapter.java @@ -21,6 +21,8 @@ import android.view.View; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.TvOptionsManager; +import com.android.tv.TvOptionsManager.OptionChangedListener; import com.android.tv.analytics.Tracker; import java.util.List; @@ -64,9 +66,12 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter< public void update() { if (mActionList == null) { mActionList = createActions(); + updateActions(); setItemList(mActionList); } else { - updateActions(); + if (updateActions()) { + setItemList(mActionList); + } } } @@ -76,7 +81,7 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter< } protected abstract List<MenuAction> createActions(); - protected abstract void updateActions(); + protected abstract boolean updateActions(); protected abstract void executeAction(int type); /** @@ -88,6 +93,37 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter< return mActionList.get(position); } + /** + * Sets the action at the given position. + * Note that action at the position may differ from returned by {@link #createActions}. + * See {@link CustomizableOptionsRowAdapter} + */ + protected void setAction(int position, MenuAction action) { + mActionList.set(position, action); + } + + /** + * Adds an action to the given position. + * Note that action at the position may differ from returned by {@link #createActions}. + * See {@link CustomizableOptionsRowAdapter} + */ + protected void addAction(int position, MenuAction action) { + mActionList.add(position, action); + } + + /** + * Removes an action at the given position. + * Note that action at the position may differ from returned by {@link #createActions}. + * See {@link CustomizableOptionsRowAdapter} + */ + protected void removeAction(int position) { + mActionList.remove(position); + } + + protected int getActionSize() { + return mActionList.size(); + } + @Override public void onBindViewHolder(MyViewHolder viewHolder, int position) { super.onBindViewHolder(viewHolder, position); @@ -103,4 +139,14 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter< // be preserved. return mActionList.get(position).getType(); } + + protected void setOptionChangedListener(final MenuAction action) { + TvOptionsManager om = getMainActivity().getTvOptionsManager(); + om.setOptionChangedListener(action.getType(), new OptionChangedListener() { + @Override + public void onOptionChanged(String newOption) { + setItemList(mActionList); + } + }); + } } diff --git a/src/com/android/tv/menu/PartnerOptionsRowAdapter.java b/src/com/android/tv/menu/PartnerOptionsRowAdapter.java index c8249a4c..f3e09f80 100644 --- a/src/com/android/tv/menu/PartnerOptionsRowAdapter.java +++ b/src/com/android/tv/menu/PartnerOptionsRowAdapter.java @@ -38,7 +38,8 @@ public class PartnerOptionsRowAdapter extends CustomizableOptionsRowAdapter { } @Override - protected void updateActions() { + protected boolean updateActions() { // TODO: Support adding description for custom actions. + return false; } } diff --git a/src/com/android/tv/menu/PipOptionsRowAdapter.java b/src/com/android/tv/menu/PipOptionsRowAdapter.java new file mode 100644 index 00000000..87203e9d --- /dev/null +++ b/src/com/android/tv/menu/PipOptionsRowAdapter.java @@ -0,0 +1,137 @@ +/* + * 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.content.Context; +import android.text.TextUtils; + +import com.android.tv.MainActivity; +import com.android.tv.R; +import com.android.tv.TvOptionsManager; +import com.android.tv.ui.TvViewUiManager; +import com.android.tv.ui.sidepanel.PipInputSelectorFragment; +import com.android.tv.util.PipInputManager.PipInput; +import com.android.tv.util.TvSettings; + +import java.util.ArrayList; +import java.util.List; + +/* + * An adapter of PIP options. + */ +public class PipOptionsRowAdapter extends OptionsRowAdapter { + private static final int[] DRAWABLE_ID_FOR_LAYOUT = { + R.drawable.ic_pip_option_layout1, + R.drawable.ic_pip_option_layout2, + R.drawable.ic_pip_option_layout3, + R.drawable.ic_pip_option_layout4, + R.drawable.ic_pip_option_layout5 }; + + private final TvOptionsManager mTvOptionsManager; + private final TvViewUiManager mTvViewUiManager; + + public PipOptionsRowAdapter(Context context) { + super(context); + mTvOptionsManager = getMainActivity().getTvOptionsManager(); + mTvViewUiManager = getMainActivity().getTvViewUiManager(); + } + + @Override + protected List<MenuAction> createActions() { + List<MenuAction> actionList = new ArrayList<>(); + actionList.add(MenuAction.PIP_SELECT_INPUT_ACTION); + actionList.add(MenuAction.PIP_SWAP_ACTION); + actionList.add(MenuAction.PIP_SOUND_ACTION); + actionList.add(MenuAction.PIP_LAYOUT_ACTION); + actionList.add(MenuAction.PIP_SIZE_ACTION); + for (MenuAction action : actionList) { + setOptionChangedListener(action); + } + return actionList; + } + + @Override + public boolean updateActions() { + boolean changed = false; + if (updateSelectInputAction()) { + changed = true; + } + if (updateLayoutAction()) { + changed = true; + } + if (updateSizeAction()) { + changed = true; + } + return changed; + } + + private boolean updateSelectInputAction() { + String oldInputLabel = mTvOptionsManager.getOptionString(TvOptionsManager.OPTION_PIP_INPUT); + + MainActivity tvActivity = getMainActivity(); + PipInput newInput = tvActivity.getPipInputManager().getPipInput(tvActivity.getPipChannel()); + String newInputLabel = newInput == null ? null : newInput.getLabel(); + + if (!TextUtils.equals(oldInputLabel, newInputLabel)) { + mTvOptionsManager.onPipInputChanged(newInputLabel); + return true; + } + return false; + } + + private boolean updateLayoutAction() { + return MenuAction.PIP_LAYOUT_ACTION.setDrawableResId( + DRAWABLE_ID_FOR_LAYOUT[mTvViewUiManager.getPipLayout()]); + } + + private boolean updateSizeAction() { + boolean oldEnabled = MenuAction.PIP_SIZE_ACTION.isEnabled(); + boolean newEnabled = mTvViewUiManager.getPipLayout() != TvSettings.PIP_LAYOUT_SIDE_BY_SIDE; + if (oldEnabled != newEnabled) { + MenuAction.PIP_SIZE_ACTION.setEnabled(newEnabled); + return true; + } + return false; + } + + @Override + protected void executeAction(int type) { + switch (type) { + case TvOptionsManager.OPTION_PIP_INPUT: + getMainActivity().getOverlayManager().getSideFragmentManager().show( + new PipInputSelectorFragment()); + break; + case TvOptionsManager.OPTION_PIP_SWAP: + getMainActivity().swapPip(); + break; + case TvOptionsManager.OPTION_PIP_SOUND: + getMainActivity().togglePipSoundMode(); + break; + case TvOptionsManager.OPTION_PIP_LAYOUT: + int oldLayout = mTvViewUiManager.getPipLayout(); + int newLayout = (oldLayout + 1) % (TvSettings.PIP_LAYOUT_LAST + 1); + mTvViewUiManager.setPipLayout(newLayout, true); + MenuAction.PIP_LAYOUT_ACTION.setDrawableResId(DRAWABLE_ID_FOR_LAYOUT[newLayout]); + break; + case TvOptionsManager.OPTION_PIP_SIZE: + int oldSize = mTvViewUiManager.getPipSize(); + int newSize = (oldSize + 1) % (TvSettings.PIP_SIZE_LAST + 1); + mTvViewUiManager.setPipSize(newSize, true); + break; + } + } +} diff --git a/src/com/android/tv/menu/PlayControlsButton.java b/src/com/android/tv/menu/PlayControlsButton.java index 77715f28..aff39db3 100644 --- a/src/com/android/tv/menu/PlayControlsButton.java +++ b/src/com/android/tv/menu/PlayControlsButton.java @@ -39,9 +39,6 @@ public class PlayControlsButton extends FrameLayout { private final int mIconColor; private int mIconFocusedColor; - private int mImageResourceId; - private int mTintColor; - public PlayControlsButton(Context context) { this(context, null); } @@ -70,21 +67,10 @@ public class PlayControlsButton extends FrameLayout { * Sets the resource ID of the image to be displayed in the center of this control. */ public void setImageResId(int imageResId) { - int newTintColor = hasFocus() ? mIconFocusedColor : mIconColor; - if (mImageResourceId != imageResId) { - mImageResourceId = imageResId; - mIcon.setImageResource(imageResId); - updateTint(newTintColor); - } else if (newTintColor != mTintColor) { - updateTint(newTintColor); - } - } - - private void updateTint(int tintColor) { - mTintColor = tintColor; - // Since on focus changing, icons' color should be switched with animation, + mIcon.setImageResource(imageResId); + // Since on foucus changing, icons' color should be switched with animation, // as a result, selectors cannot be used to switch colors in this case. - mIcon.getDrawable().setTint(tintColor); + mIcon.getDrawable().setTint(hasFocus() ? mIconFocusedColor : mIconColor); } /** @@ -131,9 +117,7 @@ public class PlayControlsButton extends FrameLayout { } else { mIcon.setVisibility(View.GONE); mLabel.setVisibility(View.VISIBLE); - if (!TextUtils.equals(mLabel.getText(), label)) { - mLabel.setText(label); - } + mLabel.setText(label); } } diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java index 4d766788..a620d4dd 100644 --- a/src/com/android/tv/menu/PlayControlsRowView.java +++ b/src/com/android/tv/menu/PlayControlsRowView.java @@ -18,10 +18,10 @@ package com.android.tv.menu; import android.content.Context; import android.content.res.Resources; -import android.text.TextUtils; import android.text.format.DateFormat; import android.util.AttributeSet; import android.view.View; +import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; @@ -34,16 +34,17 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; import com.android.tv.data.Program; -import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.ui.DvrStopRecordingFragment; -import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.dvr.ui.HalfSizedDialogFragment; import com.android.tv.menu.Menu.MenuShowReason; import com.android.tv.ui.TunableTvView; +import com.android.tv.util.Utils; public class PlayControlsRowView extends MenuRowView { private static final int NORMAL_WIDTH_MAX_BUTTON_COUNT = 5; @@ -52,10 +53,14 @@ public class PlayControlsRowView extends MenuRowView { private final int mTimeTextLeftMargin; private final int mTimelineWidth; // Views - private TextView mBackgroundView; + private View mBackgroundView; private View mTimeIndicator; private TextView mTimeText; - private PlaybackProgressBar mProgress; + private View mProgressEmptyBefore; + private View mProgressWatched; + private View mProgressBuffered; + private View mProgressEmptyAfter; + private View mControlBar; private PlayControlsButton mJumpPreviousButton; private PlayControlsButton mRewindButton; private PlayControlsButton mPlayPauseButton; @@ -64,6 +69,7 @@ public class PlayControlsRowView extends MenuRowView { private PlayControlsButton mRecordButton; private TextView mProgramStartTimeText; private TextView mProgramEndTimeText; + private View mUnavailableMessageText; private TunableTvView mTvView; private TimeShiftManager mTimeShiftManager; private final DvrDataManager mDvrDataManager; @@ -77,8 +83,6 @@ public class PlayControlsRowView extends MenuRowView { private final int mNormalButtonMargin; private final int mCompactButtonMargin; - private final String mUnavailableMessage; - private final ScheduledRecordingListener mScheduledRecordingListener = new ScheduledRecordingListener() { @Override @@ -134,7 +138,6 @@ public class PlayControlsRowView extends MenuRowView { mDvrManager = null; } mMainActivity = (MainActivity) context; - mUnavailableMessage = res.getString(R.string.play_controls_unavailable); } @Override @@ -168,10 +171,14 @@ public class PlayControlsRowView extends MenuRowView { super.onFinishInflate(); // Clip the ViewGroup(body) to the rounded rectangle of outline. findViewById(R.id.body).setClipToOutline(true); - mBackgroundView = (TextView) findViewById(R.id.background); + mBackgroundView = findViewById(R.id.background); mTimeIndicator = findViewById(R.id.time_indicator); mTimeText = (TextView) findViewById(R.id.time_text); - mProgress = (PlaybackProgressBar) findViewById(R.id.progress); + mProgressEmptyBefore = findViewById(R.id.timeline_bg_start); + mProgressWatched = findViewById(R.id.watched); + mProgressBuffered = findViewById(R.id.buffered); + mProgressEmptyAfter = findViewById(R.id.timeline_bg_end); + mControlBar = findViewById(R.id.play_control_bar); mJumpPreviousButton = (PlayControlsButton) findViewById(R.id.jump_previous); mRewindButton = (PlayControlsButton) findViewById(R.id.rewind); mPlayPauseButton = (PlayControlsButton) findViewById(R.id.play_pause); @@ -180,6 +187,7 @@ public class PlayControlsRowView extends MenuRowView { mRecordButton = (PlayControlsButton) findViewById(R.id.record); mProgramStartTimeText = (TextView) findViewById(R.id.program_start_time); mProgramEndTimeText = (TextView) findViewById(R.id.program_end_time); + mUnavailableMessageText = findViewById(R.id.unavailable_text); initializeButton(mJumpPreviousButton, R.drawable.lb_ic_skip_previous, R.string.play_controls_description_skip_previous, null, new Runnable() { @@ -187,7 +195,7 @@ public class PlayControlsRowView extends MenuRowView { public void run() { if (mTimeShiftManager.isAvailable()) { mTimeShiftManager.jumpToPrevious(); - updateControls(true); + updateControls(); } } }); @@ -227,7 +235,7 @@ public class PlayControlsRowView extends MenuRowView { public void run() { if (mTimeShiftManager.isAvailable()) { mTimeShiftManager.jumpToNext(); - updateControls(true); + updateControls(); } } }); @@ -257,17 +265,18 @@ public class PlayControlsRowView extends MenuRowView { if (!(mDvrManager != null && mDvrManager.isChannelRecordable(currentChannel))) { Toast.makeText(mMainActivity, R.string.dvr_msg_cannot_record_channel, Toast.LENGTH_SHORT).show(); - } else { + } else if (DvrUiHelper.checkStorageStatusAndShowErrorMessage(mMainActivity, + currentChannel.getInputId())) { Program program = TvApplication.getSingletons(mMainActivity).getProgramDataManager() .getCurrentProgram(currentChannel.getId()); - DvrUiHelper.checkStorageStatusAndShowErrorMessage(mMainActivity, - currentChannel.getInputId(), new Runnable() { - @Override - public void run() { - DvrUiHelper.requestRecordingCurrentProgram(mMainActivity, - currentChannel, program, true); - } - }); + if (program == null) { + DvrUiHelper.showChannelRecordDurationOptions(mMainActivity, currentChannel); + } else if (DvrUiHelper.handleCreateSchedule(mMainActivity, program)) { + String msg = mMainActivity.getString(R.string.dvr_msg_current_program_scheduled, + program.getTitle(), + Utils.toTimeString(program.getEndTimeUtcMillis(), false)); + Toast.makeText(mMainActivity, msg, Toast.LENGTH_SHORT).show(); + } } } else if (currentChannel != null) { DvrUiHelper.showStopRecordingDialog(mMainActivity, currentChannel.getId(), @@ -309,37 +318,39 @@ public class PlayControlsRowView extends MenuRowView { @Override public void onAvailabilityChanged() { updateMenuVisibility(); - PlayControlsRowView.this.updateAll(false); + if (isShown()) { + PlayControlsRowView.this.updateAll(); + } } @Override public void onPlayStatusChanged(int status) { updateMenuVisibility(); - if (mTimeShiftManager.isAvailable()) { - updateControls(false); + if (mTimeShiftManager.isAvailable() && isShown()) { + updateControls(); } } @Override public void onRecordTimeRangeChanged() { - if (mTimeShiftManager.isAvailable()) { - updateControls(false); + if (mTimeShiftManager.isAvailable() && isShown()) { + updateControls(); } } @Override public void onCurrentPositionChanged() { - if (mTimeShiftManager.isAvailable()) { + if (mTimeShiftManager.isAvailable() && isShown()) { initializeTimeline(); - updateControls(false); + updateControls(); } } @Override public void onProgramInfoChanged() { - if (mTimeShiftManager.isAvailable()) { + if (mTimeShiftManager.isAvailable() && isShown()) { initializeTimeline(); - updateControls(false); + updateControls(); } } @@ -361,8 +372,7 @@ public class PlayControlsRowView extends MenuRowView { } } }); - // force update to initialize everything - updateAll(true); + updateAll(); } private void initializeTimeline() { @@ -370,8 +380,6 @@ public class PlayControlsRowView extends MenuRowView { mTimeShiftManager.getCurrentPositionMs()); mProgramStartTimeMs = program.getStartTimeUtcMillis(); mProgramEndTimeMs = program.getEndTimeUtcMillis(); - mProgress.setMax(mProgramEndTimeMs - mProgramStartTimeMs); - updateRecTimeText(); SoftPreconditions.checkArgument(mProgramStartTimeMs <= mProgramEndTimeMs); } @@ -381,13 +389,10 @@ public class PlayControlsRowView extends MenuRowView { getMenu().setKeepVisible(keepMenuVisible); } - public void onPreselected() { - updateControls(true); - } - @Override public void onSelected(boolean showTitle) { super.onSelected(showTitle); + updateControls(); postHideRippleAnimation(); } @@ -469,32 +474,28 @@ public class PlayControlsRowView extends MenuRowView { * Updates the view contents. It is called from the PlayControlsRow. */ public void update() { - updateAll(false); + updateAll(); } - private void updateAll(boolean forceUpdate) { + private void updateAll() { if (mTimeShiftManager.isAvailable() && !mTvView.isScreenBlocked()) { setEnabled(true); initializeTimeline(); mBackgroundView.setEnabled(true); - setTextIfNeeded(mBackgroundView, null); } else { setEnabled(false); mBackgroundView.setEnabled(false); - setTextIfNeeded(mBackgroundView, mUnavailableMessage); } - // force the controls be updated no matter it's visible or not. - updateControls(forceUpdate); + updateControls(); } - private void updateControls(boolean forceUpdate) { - if (forceUpdate || getContentsView().isShown()) { - updateTime(); - updateProgress(); - updateButtons(); - updateRecordButton(); - updateButtonMargin(); - } + private void updateControls() { + updateTime(); + updateProgress(); + updateRecTimeText(); + updateButtons(); + updateRecordButton(); + updateButtonMargin(); } private void updateTime() { @@ -503,39 +504,70 @@ public class PlayControlsRowView extends MenuRowView { mTimeIndicator.setVisibility(View.VISIBLE); } else { mTimeText.setVisibility(View.INVISIBLE); - mTimeIndicator.setVisibility(View.GONE); + mTimeIndicator.setVisibility(View.INVISIBLE); return; } long currentPositionMs = mTimeShiftManager.getCurrentPositionMs(); + ViewGroup.MarginLayoutParams params = + (ViewGroup.MarginLayoutParams) mTimeText.getLayoutParams(); int currentTimePositionPixel = convertDurationToPixel(currentPositionMs - mProgramStartTimeMs); - mTimeText.setTranslationX(currentTimePositionPixel + mTimeTextLeftMargin); - setTextIfNeeded(mTimeText, getTimeString(currentPositionMs)); - mTimeIndicator.setTranslationX(currentTimePositionPixel + mTimeIndicatorLeftMargin); + params.leftMargin = currentTimePositionPixel + mTimeTextLeftMargin; + mTimeText.setLayoutParams(params); + mTimeText.setText(getTimeString(currentPositionMs)); + params = (ViewGroup.MarginLayoutParams) mTimeIndicator.getLayoutParams(); + params.leftMargin = currentTimePositionPixel + mTimeIndicatorLeftMargin; + mTimeIndicator.setLayoutParams(params); } private void updateProgress() { if (isEnabled()) { - long progressStartTimeMs = Math.min(mProgramEndTimeMs, + mProgressWatched.setVisibility(View.VISIBLE); + mProgressBuffered.setVisibility(View.VISIBLE); + mProgressEmptyAfter.setVisibility(View.VISIBLE); + } else { + mProgressWatched.setVisibility(View.INVISIBLE); + mProgressBuffered.setVisibility(View.INVISIBLE); + mProgressEmptyAfter.setVisibility(View.INVISIBLE); + if (mProgramStartTimeMs < mProgramEndTimeMs) { + layoutProgress(mProgressEmptyBefore, mProgramStartTimeMs, mProgramEndTimeMs); + } else { + // Not initialized yet. + layoutProgress(mProgressEmptyBefore, mTimelineWidth); + } + return; + } + + long progressStartTimeMs = Math.min(mProgramEndTimeMs, Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordStartTimeMs())); - long currentPlayingTimeMs = Math.min(mProgramEndTimeMs, + long currentPlayingTimeMs = Math.min(mProgramEndTimeMs, Math.max(mProgramStartTimeMs, mTimeShiftManager.getCurrentPositionMs())); - long progressEndTimeMs = Math.min(mProgramEndTimeMs, + long progressEndTimeMs = Math.min(mProgramEndTimeMs, Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordEndTimeMs())); - mProgress.setProgressRange(progressStartTimeMs - mProgramStartTimeMs, - progressEndTimeMs - mProgramStartTimeMs); - mProgress.setProgress(currentPlayingTimeMs - mProgramStartTimeMs); - } else { - mProgress.setProgressRange(0, 0); - } + + layoutProgress(mProgressEmptyBefore, mProgramStartTimeMs, progressStartTimeMs); + layoutProgress(mProgressWatched, progressStartTimeMs, currentPlayingTimeMs); + layoutProgress(mProgressBuffered, currentPlayingTimeMs, progressEndTimeMs); + } + + private void layoutProgress(View progress, long progressStartTimeMs, long progressEndTimeMs) { + layoutProgress(progress, Math.max(0, + convertDurationToPixel(progressEndTimeMs - progressStartTimeMs)) + 1); + } + + private void layoutProgress(View progress, int width) { + ViewGroup.MarginLayoutParams params = + (ViewGroup.MarginLayoutParams) progress.getLayoutParams(); + params.width = width; + progress.setLayoutParams(params); } private void updateRecTimeText() { if (isEnabled()) { mProgramStartTimeText.setVisibility(View.VISIBLE); - setTextIfNeeded(mProgramStartTimeText, getTimeString(mProgramStartTimeMs)); + mProgramStartTimeText.setText(getTimeString(mProgramStartTimeMs)); mProgramEndTimeText.setVisibility(View.VISIBLE); - setTextIfNeeded(mProgramEndTimeText, getTimeString(mProgramEndTimeMs)); + mProgramEndTimeText.setText(getTimeString(mProgramEndTimeMs)); } else { mProgramStartTimeText.setVisibility(View.GONE); mProgramEndTimeText.setVisibility(View.GONE); @@ -544,17 +576,11 @@ public class PlayControlsRowView extends MenuRowView { private void updateButtons() { if (isEnabled()) { - mPlayPauseButton.setVisibility(View.VISIBLE); - mJumpPreviousButton.setVisibility(View.VISIBLE); - mJumpNextButton.setVisibility(View.VISIBLE); - mRewindButton.setVisibility(View.VISIBLE); - mFastForwardButton.setVisibility(View.VISIBLE); + mControlBar.setVisibility(View.VISIBLE); + mUnavailableMessageText.setVisibility(View.GONE); } else { - mPlayPauseButton.setVisibility(View.GONE); - mJumpPreviousButton.setVisibility(View.GONE); - mJumpNextButton.setVisibility(View.GONE); - mRewindButton.setVisibility(View.GONE); - mFastForwardButton.setVisibility(View.GONE); + mControlBar.setVisibility(View.INVISIBLE); + mUnavailableMessageText.setVisibility(View.VISIBLE); return; } @@ -596,12 +622,6 @@ public class PlayControlsRowView extends MenuRowView { } private void updateRecordButton() { - if (isEnabled()) { - mRecordButton.setVisibility(VISIBLE); - } else { - mRecordButton.setVisibility(GONE); - return; - } if (!(mDvrManager != null && mDvrManager.isChannelRecordable(mMainActivity.getCurrentChannel()))) { mRecordButton.setVisibility(View.GONE); @@ -662,10 +682,4 @@ public class PlayControlsRowView extends MenuRowView { mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener); } } - - private void setTextIfNeeded(TextView textView, String text) { - if (!TextUtils.equals(textView.getText(), text)) { - textView.setText(text); - } - } } diff --git a/src/com/android/tv/menu/PlaybackProgressBar.java b/src/com/android/tv/menu/PlaybackProgressBar.java deleted file mode 100644 index e8061bc6..00000000 --- a/src/com/android/tv/menu/PlaybackProgressBar.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright (C) 2017 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.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.util.AttributeSet; -import android.view.View; - -import com.android.tv.R; - -/** - * A progress bar control which has two progresses which start in the middle of the control. - */ -public class PlaybackProgressBar extends View { - private final LayerDrawable mProgressDrawable; - private final Drawable mPrimaryDrawable; - private final Drawable mSecondaryDrawable; - private long mMax = 100; - private long mProgressStart = 0; - private long mProgressEnd = 0; - private long mProgress = 0; - - public PlaybackProgressBar(Context context) { - this(context, null); - } - - public PlaybackProgressBar(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public PlaybackProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public PlaybackProgressBar(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes( - attrs, R.styleable.PlaybackProgressBar, defStyleAttr, defStyleRes); - mProgressDrawable = - (LayerDrawable) a.getDrawable(R.styleable.PlaybackProgressBar_progressDrawable); - mPrimaryDrawable = mProgressDrawable.findDrawableByLayerId(android.R.id.progress); - mSecondaryDrawable = - mProgressDrawable.findDrawableByLayerId(android.R.id.secondaryProgress); - a.recycle(); - refreshProgress(); - } - - @Override - protected void onDraw(Canvas canvas) { - final int saveCount = canvas.save(); - canvas.translate(getPaddingLeft(), getPaddingTop()); - mProgressDrawable.draw(canvas); - canvas.restoreToCount(saveCount); - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - refreshProgress(); - } - - public void setMax(long max) { - if (max < 0) { - max = 0; - } - if (max != mMax) { - mMax = max; - if (mProgressStart > max) { - mProgressStart = max; - } - if (mProgressEnd > max) { - mProgressEnd = max; - } - if (mProgress > max) { - mProgress = max; - } - refreshProgress(); - } - } - - /** - * Sets the start and end position of the progress. - */ - public void setProgressRange(long start, long end) { - start = constrain(start, 0, mMax); - end = constrain(end, start, mMax); - mProgress = constrain(mProgress, start, end); - if (start != mProgressStart || end != mProgressEnd) { - mProgressStart = start; - mProgressEnd = end; - setProgressLevels(); - } - } - - /** - * Sets the progress position. - */ - public void setProgress(long progress) { - progress = constrain(progress, mProgressStart, mProgressEnd); - if (progress != mProgress) { - mProgress = progress; - setProgressLevels(); - } - } - - private long constrain(long value, long min, long max) { - return Math.min(Math.max(value, min), max); - } - - private void refreshProgress() { - int width = getWidth() - getPaddingStart() - getPaddingEnd(); - int height = getHeight() - getPaddingTop() - getPaddingBottom(); - mProgressDrawable.setBounds(0, 0, width, height); - setProgressLevels(); - } - - private void setProgressLevels() { - boolean progressUpdated = setProgressBound(mPrimaryDrawable, mProgressStart, mProgress); - progressUpdated |= setProgressBound(mSecondaryDrawable, mProgress, mProgressEnd); - if (progressUpdated) { - postInvalidate(); - } - } - - private boolean setProgressBound(Drawable drawable, long start, long end) { - Rect oldBounds = drawable.getBounds(); - if (mMax == 0) { - if (!isEqualRect(oldBounds, 0, 0, 0, 0)) { - drawable.setBounds(0, 0, 0, 0); - return true; - } - return false; - } - int width = mProgressDrawable.getBounds().width(); - int height = mProgressDrawable.getBounds().height(); - int left = (int) (width * start / mMax); - int right = (int) (width * end / mMax); - if (!isEqualRect(oldBounds, left, 0, right, height)) { - drawable.setBounds(left, 0, right, height); - return true; - } - return false; - } - - private boolean isEqualRect(Rect rect, int left, int top, int right, int bottom) { - return rect.left == left && rect.top == top && rect.right == right && rect.bottom == bottom; - } -} diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java index 220fcd3a..fb062246 100644 --- a/src/com/android/tv/menu/TvOptionsRowAdapter.java +++ b/src/com/android/tv/menu/TvOptionsRowAdapter.java @@ -21,6 +21,7 @@ import android.media.tv.TvTrackInfo; import android.support.annotation.VisibleForTesting; import com.android.tv.Features; +import com.android.tv.R; import com.android.tv.TvOptionsManager; import com.android.tv.customization.CustomAction; import com.android.tv.data.DisplayMode; @@ -29,6 +30,7 @@ import com.android.tv.ui.sidepanel.ClosedCaptionFragment; import com.android.tv.ui.sidepanel.DeveloperOptionFragment; import com.android.tv.ui.sidepanel.DisplayModeFragment; import com.android.tv.ui.sidepanel.MultiAudioFragment; +import com.android.tv.util.PipInputManager; import java.util.ArrayList; import java.util.List; @@ -37,6 +39,12 @@ import java.util.List; * An adapter of options. */ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { + private static final boolean ENABLE_IN_APP_PIP = false; + + private int mPositionPipAction; + // If mInAppPipAction is false, system-wide PIP is used. + private boolean mInAppPipAction = true; + public TvOptionsRowAdapter(Context context, List<CustomAction> customActions) { super(context, customActions); } @@ -45,62 +53,123 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { protected List<MenuAction> createBaseActions() { List<MenuAction> actionList = new ArrayList<>(); actionList.add(MenuAction.SELECT_CLOSED_CAPTION_ACTION); + setOptionChangedListener(MenuAction.SELECT_CLOSED_CAPTION_ACTION); actionList.add(MenuAction.SELECT_DISPLAY_MODE_ACTION); - if (Features.PICTURE_IN_PICTURE.isEnabled(getMainActivity())) { - actionList.add(MenuAction.SYSTEMWIDE_PIP_ACTION); - } + setOptionChangedListener(MenuAction.SELECT_DISPLAY_MODE_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); actionList.add(MenuAction.MORE_CHANNELS_ACTION); if (DeveloperOptionFragment.shouldShow()) { actionList.add(MenuAction.DEV_ACTION); } actionList.add(MenuAction.SETTINGS_ACTION); - updateClosedCaptionAction(); - updateMultiAudioAction(); - updateDisplayModeAction(); + if (getCustomActions() != null) { + // Adjust Pip action position which will be changed by applying custom actions. + for (CustomAction customAction : getCustomActions()) { + if (customAction.isFront()) { + mPositionPipAction++; + } + } + } + return actionList; } @Override - protected void updateActions() { - if (updateClosedCaptionAction()) { - notifyItemChanged(getItemPosition(MenuAction.SELECT_CLOSED_CAPTION_ACTION)); + protected boolean updateActions() { + boolean changed = false; + if (updatePipAction()) { + changed = true; } if (updateMultiAudioAction()) { - notifyItemChanged(getItemPosition(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION)); + changed = true; } if (updateDisplayModeAction()) { - notifyItemChanged(getItemPosition(MenuAction.SELECT_DISPLAY_MODE_ACTION)); + changed = true; } + return changed; } - @VisibleForTesting - private boolean updateClosedCaptionAction() { - return updateActionDescription(MenuAction.SELECT_CLOSED_CAPTION_ACTION); + private boolean updatePipAction() { + // There are four states. + // Case 1. The device doesn't even have any input for PIP. (e.g. OTT box without HDMI input) + // => Remove the icon. + // Case 2. The device has one or more inputs for PIP but none of them are currently + // available. + // => Show the icon but disable it. + // Case 3. The device has one or more available PIP inputs and now it's tuned off. + // => Show the icon with "Off". + // Case 4. The device has one or more available PIP inputs but it's already turned on. + // => Show the icon with "On". + + boolean changed = false; + + // Case 1 + PipInputManager pipInputManager = getMainActivity().getPipInputManager(); + if (ENABLE_IN_APP_PIP && pipInputManager.getPipInputSize(false) > 1) { + if (!mInAppPipAction) { + removeAction(mPositionPipAction); + addAction(mPositionPipAction, MenuAction.PIP_IN_APP_ACTION); + mInAppPipAction = true; + changed = true; + } + } else { + if (mInAppPipAction) { + removeAction(mPositionPipAction); + mInAppPipAction = false; + if (Features.PICTURE_IN_PICTURE.isEnabled(getMainActivity())) { + addAction(mPositionPipAction, MenuAction.SYSTEMWIDE_PIP_ACTION); + } + return true; + } + return false; + } + + // Case 2 + boolean isPipEnabled = getMainActivity().isPipEnabled(); + 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_IN_APP_ACTION.setEnabled(newEnabled); + changed = true; + } + } + + // Case 3 & 4 - we just need to update the icon. + MenuAction.PIP_IN_APP_ACTION.setDrawableResId( + isPipEnabled ? R.drawable.ic_tvoption_pip : R.drawable.ic_tvoption_pip_off); + return changed; } @VisibleForTesting boolean updateMultiAudioAction() { List<TvTrackInfo> audioTracks = getMainActivity().getTracks(TvTrackInfo.TYPE_AUDIO); - boolean enabled = audioTracks != null && audioTracks.size() > 1; - // Use "|" operator for non-short-circuit evaluation. - return MenuAction.setEnabled(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION, enabled) - | updateActionDescription(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION); + boolean oldEnabled = MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled(); + boolean newEnabled = audioTracks != null && audioTracks.size() > 1; + if (oldEnabled != newEnabled) { + MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.setEnabled(newEnabled); + return true; + } + return false; } private boolean updateDisplayModeAction() { TvViewUiManager uiManager = getMainActivity().getTvViewUiManager(); - boolean enabled = uiManager.isDisplayModeAvailable(DisplayMode.MODE_FULL) + boolean oldEnabled = MenuAction.SELECT_DISPLAY_MODE_ACTION.isEnabled(); + boolean newEnabled = uiManager.isDisplayModeAvailable(DisplayMode.MODE_FULL) || uiManager.isDisplayModeAvailable(DisplayMode.MODE_ZOOM); - // Use "|" operator for non-short-circuit evaluation. - return MenuAction.setEnabled(MenuAction.SELECT_DISPLAY_MODE_ACTION, enabled) - | updateActionDescription(MenuAction.SELECT_DISPLAY_MODE_ACTION); - } - - private boolean updateActionDescription(MenuAction action) { - return MenuAction.setActionDescription(action, - getMainActivity().getTvOptionsManager().getOptionString(action.getType())); + if (oldEnabled != newEnabled) { + MenuAction.SELECT_DISPLAY_MODE_ACTION.setEnabled(newEnabled); + return true; + } + return false; } @Override @@ -114,6 +183,9 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { getMainActivity().getOverlayManager().getSideFragmentManager() .show(new DisplayModeFragment()); break; + case TvOptionsManager.OPTION_IN_APP_PIP: + getMainActivity().togglePipView(); + break; case TvOptionsManager.OPTION_SYSTEMWIDE_PIP: getMainActivity().enterPictureInPictureMode(); break; @@ -133,4 +205,4 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { break; } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/onboarding/SetupSourcesFragment.java b/src/com/android/tv/onboarding/SetupSourcesFragment.java index f56daec5..7607822c 100644 --- a/src/com/android/tv/onboarding/SetupSourcesFragment.java +++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java @@ -38,7 +38,6 @@ import com.android.tv.common.ui.setup.SetupGuidedStepFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.TvInputNewComparator; -import com.android.tv.tuner.TunerInputController; import com.android.tv.ui.GuidedActionsStylistWithDivider; import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; @@ -205,7 +204,6 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { mChannelDataManager.addListener(mChannelDataManagerListener); super.onCreate(savedInstanceState); mParentFragment = (SetupSourcesFragment) getParentFragment(); - TunerInputController.executeNetworkTunerDiscoveryAsyncTask(getContext()); } @Override diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java index 03d7873f..8d6c5a14 100644 --- a/src/com/android/tv/receiver/BootCompletedReceiver.java +++ b/src/com/android/tv/receiver/BootCompletedReceiver.java @@ -27,7 +27,7 @@ import com.android.tv.Features; import com.android.tv.TvActivity; import com.android.tv.TvApplication; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.dvr.recorder.DvrRecordingService; +import com.android.tv.dvr.DvrRecordingService; import com.android.tv.recommendation.NotificationService; import com.android.tv.util.OnboardingUtils; import com.android.tv.util.SetupUtils; diff --git a/src/com/android/tv/receiver/GlobalKeyReceiver.java b/src/com/android/tv/receiver/GlobalKeyReceiver.java index 2d9ee10e..8cd4fdf1 100644 --- a/src/com/android/tv/receiver/GlobalKeyReceiver.java +++ b/src/com/android/tv/receiver/GlobalKeyReceiver.java @@ -20,8 +20,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.media.tv.TvContract; -import android.os.AsyncTask; -import android.provider.Settings; import android.util.Log; import android.view.KeyEvent; @@ -33,57 +31,27 @@ import com.android.tv.TvApplication; public class GlobalKeyReceiver extends BroadcastReceiver { private static final boolean DEBUG = false; private static final String TAG = "GlobalKeyReceiver"; - private static final String ACTION_GLOBAL_BUTTON = "android.intent.action.GLOBAL_BUTTON"; - // Settings.Secure.USER_SETUP_COMPLETE is hidden. - private static final String SETTINGS_USER_SETUP_COMPLETE = "user_setup_complete"; - - private static boolean sUserSetupComplete; @Override public void onReceive(Context context, Intent intent) { TvApplication.setCurrentRunningProcess(context, true); - Context appContext = context.getApplicationContext(); - if (DEBUG) Log.d(TAG, "onReceive: " + intent); - if (sUserSetupComplete) { - handleIntent(appContext, intent); - } else { - new AsyncTask<Void, Void, Boolean>() { - @Override - protected Boolean doInBackground(Void... params) { - return Settings.Secure.getInt(appContext.getContentResolver(), - SETTINGS_USER_SETUP_COMPLETE, 0) != 0; - } - - @Override - protected void onPostExecute(Boolean setupComplete) { - if (DEBUG) Log.d(TAG, "Is setup complete: " + setupComplete); - sUserSetupComplete = setupComplete; - if (sUserSetupComplete) { - handleIntent(appContext, intent); - } - } - }.execute(); - } - } - - private void handleIntent(Context appContext, Intent intent) { if (ACTION_GLOBAL_BUTTON.equals(intent.getAction())) { KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); - if (DEBUG) Log.d(TAG, "handleIntent: " + event); + if (DEBUG) Log.d(TAG, "onReceive: " + event); int keyCode = event.getKeyCode(); int action = event.getAction(); if (action == KeyEvent.ACTION_UP) { switch (keyCode) { case KeyEvent.KEYCODE_GUIDE: - appContext.startActivity( + context.startActivity( new Intent(Intent.ACTION_VIEW, TvContract.Programs.CONTENT_URI)); break; case KeyEvent.KEYCODE_TV: - ((TvApplication) appContext).handleTvKey(); + ((TvApplication) context.getApplicationContext()).handleTvKey(); break; case KeyEvent.KEYCODE_TV_INPUT: - ((TvApplication) appContext).handleTvInputKey(); + ((TvApplication) context.getApplicationContext()).handleTvInputKey(); break; default: // Do nothing diff --git a/src/com/android/tv/receiver/PackageIntentsReceiver.java b/src/com/android/tv/receiver/PackageIntentsReceiver.java index 2d3f8705..26d000e7 100644 --- a/src/com/android/tv/receiver/PackageIntentsReceiver.java +++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java @@ -19,10 +19,8 @@ package com.android.tv.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.net.Uri; import com.android.tv.TvApplication; -import com.android.tv.util.Partner; /** * A class for handling the broadcast intents from PackageManager. @@ -33,9 +31,5 @@ public class PackageIntentsReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { TvApplication.setCurrentRunningProcess(context, true); ((TvApplication) context.getApplicationContext()).handleInputCountChanged(); - - Uri uri = intent.getData(); - final String packageName = (uri != null ? uri.getSchemeSpecificPart() : null); - Partner.reset(context, packageName); } } diff --git a/src/com/android/tv/recommendation/NotificationService.java b/src/com/android/tv/recommendation/NotificationService.java index a472f559..30ec73e3 100644 --- a/src/com/android/tv/recommendation/NotificationService.java +++ b/src/com/android/tv/recommendation/NotificationService.java @@ -426,7 +426,6 @@ public class NotificationService extends Service implements Recommender.Listener : 100 - (int) (programLeftTimsMs * 100 / programDurationMs); Intent intent = new Intent(Intent.ACTION_VIEW, channel.getUri()); intent.putExtra(TUNE_PARAMS_RECOMMENDATION_TYPE, mRecommendationType); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); final PendingIntent notificationIntent = PendingIntent.getActivity(this, 0, intent, 0); // This callback will run on the main thread. diff --git a/src/com/android/tv/search/DataManagerSearch.java b/src/com/android/tv/search/DataManagerSearch.java index d90908f1..5f89a21a 100644 --- a/src/com/android/tv/search/DataManagerSearch.java +++ b/src/com/android/tv/search/DataManagerSearch.java @@ -265,7 +265,9 @@ public class DataManagerSearch implements SearchInterface { } private String buildIntentData(long channelId) { - return TvContract.buildChannelUri(channelId).toString(); + return TvContract.buildChannelUri(channelId).buildUpon() + .appendQueryParameter(Utils.PARAM_SOURCE, SOURCE_TV_SEARCH) + .build().toString(); } private boolean isRatingBlocked(TvContentRating[] ratings) { diff --git a/src/com/android/tv/search/SearchInterface.java b/src/com/android/tv/search/SearchInterface.java index c9a63128..caa45812 100644 --- a/src/com/android/tv/search/SearchInterface.java +++ b/src/com/android/tv/search/SearchInterface.java @@ -24,6 +24,8 @@ import java.util.List; * Interface for channel and program search. */ public interface SearchInterface { + String SOURCE_TV_SEARCH = "TvSearch"; + int ACTION_TYPE_AMBIGUOUS = 1; int ACTION_TYPE_SWITCH_CHANNEL = 2; int ACTION_TYPE_SWITCH_INPUT = 3; diff --git a/src/com/android/tv/search/TvProviderSearch.java b/src/com/android/tv/search/TvProviderSearch.java index ea144786..2ceec19a 100644 --- a/src/com/android/tv/search/TvProviderSearch.java +++ b/src/com/android/tv/search/TvProviderSearch.java @@ -38,8 +38,6 @@ import com.android.tv.search.LocalSearchProvider.SearchResult; import com.android.tv.util.PermissionUtils; import com.android.tv.util.Utils; -import junit.framework.Assert; - import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -191,10 +189,6 @@ public class TvProviderSearch implements SearchInterface { @WorkerThread private List<SearchResult> searchChannels(String query, String[] columnForExactMatching, String[] columnForPartialMatching, Set<Long> channelsFound, int limit) { - Assert.assertTrue( - (columnForExactMatching != null && columnForExactMatching.length > 0) || - (columnForPartialMatching != null && columnForPartialMatching.length > 0)); - String[] projection = { Channels._ID, Channels.COLUMN_DISPLAY_NUMBER, @@ -314,10 +308,6 @@ public class TvProviderSearch implements SearchInterface { String[] columnForPartialMatching, Set<Long> channelsFound, int limit) { if (DEBUG) Log.d(TAG, "Searching programs: '" + query + "'"); long time = SystemClock.elapsedRealtime(); - Assert.assertTrue( - (columnForExactMatching != null && columnForExactMatching.length > 0) || - (columnForPartialMatching != null && columnForPartialMatching.length > 0)); - String[] projection = { Programs.COLUMN_CHANNEL_ID, Programs.COLUMN_TITLE, @@ -412,7 +402,9 @@ public class TvProviderSearch implements SearchInterface { } private String buildIntentData(long channelId) { - return TvContract.buildChannelUri(channelId).toString(); + return TvContract.buildChannelUri(channelId).buildUpon() + .appendQueryParameter(Utils.PARAM_SOURCE, SOURCE_TV_SEARCH) + .build().toString(); } private boolean isRatingBlocked(String ratings) { diff --git a/src/com/android/tv/tuner/ChannelScanFileParser.java b/src/com/android/tv/tuner/ChannelScanFileParser.java index 8b06aaa9..2dd36074 100644 --- a/src/com/android/tv/tuner/ChannelScanFileParser.java +++ b/src/com/android/tv/tuner/ChannelScanFileParser.java @@ -18,7 +18,7 @@ package com.android.tv.tuner; import android.util.Log; -import com.android.tv.tuner.data.Channel; +import com.android.tv.tuner.data.nano.Channel; import java.io.BufferedReader; import java.io.IOException; diff --git a/src/com/android/tv/tuner/TunerHal.java b/src/com/android/tv/tuner/TunerHal.java index 64394ea3..de19766e 100644 --- a/src/com/android/tv/tuner/TunerHal.java +++ b/src/com/android/tv/tuner/TunerHal.java @@ -20,9 +20,6 @@ import android.content.Context; import android.support.annotation.IntDef; import android.support.annotation.StringDef; import android.util.Log; -import android.util.Pair; - -import com.android.tv.Features; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -51,7 +48,6 @@ public abstract class TunerHal implements AutoCloseable { public static final int TUNER_TYPE_BUILT_IN = 1; public static final int TUNER_TYPE_USB = 2; - public static final int TUNER_TYPE_NETWORK = 3; protected static final int PID_PAT = 0; protected static final int PID_ATSC_SI_BASE = 0x1ffb; @@ -73,33 +69,31 @@ public abstract class TunerHal implements AutoCloseable { */ public synchronized static TunerHal createInstance(Context context) { TunerHal tunerHal = null; - if (useBuiltInTuner(context)) { + if (getTunerType(context) == TUNER_TYPE_BUILT_IN) { } - if (tunerHal == null && UsbTunerHal.getNumberOfDevices(context) > 0) { + if (tunerHal == null) { tunerHal = new UsbTunerHal(context); } - return tunerHal != null && tunerHal.openFirstAvailable() ? tunerHal : null; + if (tunerHal.openFirstAvailable()) { + return tunerHal; + } + return null; } /** * Gets the number of tuner devices currently present. */ - public static Pair<Integer, Integer> getTunerTypeAndCount(Context context) { - if (useBuiltInTuner(context)) { + public static int getTunerCount(Context context) { + if (getTunerType(context) == TUNER_TYPE_BUILT_IN) { } - int usbTunerCount = UsbTunerHal.getNumberOfDevices(context); - if (usbTunerCount > 0) { - return new Pair<>(TUNER_TYPE_USB, usbTunerCount); - } - return new Pair<>(null, 0); + return UsbTunerHal.getNumberOfDevices(context); } /** - * Returns if tuner input service would use built-in tuners instead of USB tuners or network - * tuners. + * Gets the type of tuner devices currently used. */ - static boolean useBuiltInTuner(Context context) { - return false; + public static int getTunerType(Context context) { + return TUNER_TYPE_USB; } protected TunerHal(Context context) { @@ -112,14 +106,6 @@ public abstract class TunerHal implements AutoCloseable { return mIsStreaming; } - /** - * Returns {@code true} if this tuner HAL can be reused to save tuning time between channels - * of the same frequency. - */ - public boolean isReusable() { - return true; - } - @Override protected void finalize() throws Throwable { super.finalize(); @@ -145,12 +131,9 @@ public abstract class TunerHal implements AutoCloseable { * * @param frequency a frequency of the channel to tune to * @param modulation a modulation method of the channel to tune to - * @param channelNumber channel number when channel number is already known. Some tuner HAL - * may use channelNumber instead of frequency for tune. * @return {@code true} if the operation was successful, {@code false} otherwise */ - public synchronized boolean tune(int frequency, @ModulationType String modulation, - String channelNumber) { + public synchronized boolean tune(int frequency, @ModulationType String modulation) { if (!isDeviceOpen()) { Log.e(TAG, "There's no available device"); return false; diff --git a/src/com/android/tv/tuner/TunerInputController.java b/src/com/android/tv/tuner/TunerInputController.java index 65bbbdd0..d89b6a0c 100644 --- a/src/com/android/tv/tuner/TunerInputController.java +++ b/src/com/android/tv/tuner/TunerInputController.java @@ -16,40 +16,30 @@ package com.android.tv.tuner; -import android.app.AlarmManager; -import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.AsyncTask; +import android.media.tv.TvInputInfo; +import android.media.tv.TvInputManager; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.os.SystemClock; -import android.preference.PreferenceManager; -import android.text.TextUtils; +import android.support.v4.os.BuildCompat; import android.util.Log; import android.widget.Toast; import com.android.tv.Features; -import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.tuner.R; import com.android.tv.tuner.setup.TunerSetupActivity; import com.android.tv.tuner.tvinput.TunerTvInputService; -import com.android.tv.tuner.util.SystemPropertiesProxy; import com.android.tv.tuner.util.TunerInputInfoUtils; -import java.text.ParseException; -import java.text.SimpleDateFormat; import java.util.Map; -import java.util.concurrent.TimeUnit; /** * Controls the package visibility of {@link TunerTvInputService}. @@ -61,39 +51,10 @@ import java.util.concurrent.TimeUnit; public class TunerInputController extends BroadcastReceiver { private static final boolean DEBUG = true; private static final String TAG = "TunerInputController"; - private static final String PREFERENCE_IS_NETWORK_TUNER_ATTACHED = "network_tuner"; - private static final String SECURITY_PATCH_LEVEL_KEY = "ro.build.version.security_patch"; - private static final String SECURITY_PATCH_LEVEL_FORMAT = "yyyy-MM-dd"; - - /** - * Action of {@link Intent} to check network connection repeatedly when it is necessary. - */ - public static final String CHECKING_NETWORK_CONNECTION = - "com.android.tv.action.CHECKING_NETWORK_CONNECTION"; - - /** - * Action of {@link Intent} when network tuner is attached. - */ - public static final String NETWORK_TUNER_ATTACHED = - "com.android.tv.action.NETWORK_TUNER_ATTACHED"; - - /** - * Action of {@link Intent} when network tuner is detached. - */ - public static final String NETWORK_TUNER_DETACHED = - "com.android.tv.action.NETWORK_TUNER_DETACHED"; - - private static final String EXTRA_CHECKING_DURATION = - "com.android.tv.action.extra.CHECKING_DURATION"; - - private static final long INITIAL_CHECKING_DURATION_MS = TimeUnit.SECONDS.toMillis(10); - private static final long MAXIMUM_CHECKING_DURATION_MS = TimeUnit.MINUTES.toMillis(10); private static final TunerDevice[] TUNER_DEVICES = { - new TunerDevice(0x2040, 0xb123, null), // WinTV-HVR-955Q - new TunerDevice(0x07ca, 0x0837, null), // AverTV Volar Hybrid Q - // WinTV-dualHD (bulk) will be supported after 2017 April security patch. - new TunerDevice(0x2040, 0x826d, "2017-04-01"), // WinTV-dualHD (bulk) + new TunerDevice(0x2040, 0xb123), // WinTV-HVR-955Q + new TunerDevice(0x07ca, 0x0837) // AverTV Volar Hybrid Q }; private static final int MSG_ENABLE_INPUT_SERVICE = 1000; @@ -109,9 +70,7 @@ public class TunerInputController extends BroadcastReceiver { if (mDvbDeviceAccessor == null) { mDvbDeviceAccessor = new DvbDeviceAccessor(context); } - boolean enabled = mDvbDeviceAccessor.isDvbDeviceAvailable(); - enableTunerTvInputService( - context, enabled, false, enabled ? TunerHal.TUNER_TYPE_USB : null); + enableTunerTvInputService(context, mDvbDeviceAccessor.isDvbDeviceAvailable()); break; } } @@ -125,35 +84,14 @@ public class TunerInputController extends BroadcastReceiver { private final int vendorId; private final int productId; - // security patch level from which the specific tuner type is supported. - private final String minSecurityLevel; - - private TunerDevice(int vendorId, int productId, String minSecurityLevel) { + private TunerDevice(int vendorId, int productId) { this.vendorId = vendorId; this.productId = productId; - this.minSecurityLevel = minSecurityLevel; } private boolean equals(UsbDevice device) { return device.getVendorId() == vendorId && device.getProductId() == productId; } - - private boolean isSupported(String currentSecurityLevel) { - if (minSecurityLevel == null) { - return true; - } - - long supportSecurityLevelTimeStamp = 0; - long currentSecurityLevelTimestamp = 0; - try { - SimpleDateFormat format = new SimpleDateFormat(SECURITY_PATCH_LEVEL_FORMAT); - supportSecurityLevelTimeStamp = format.parse(minSecurityLevel).getTime(); - currentSecurityLevelTimestamp = format.parse(currentSecurityLevel).getTime(); - } catch (ParseException e) { - } - return supportSecurityLevelTimeStamp != 0 - && supportSecurityLevelTimeStamp <= currentSecurityLevelTimestamp; - } } @Override @@ -161,20 +99,17 @@ public class TunerInputController extends BroadcastReceiver { if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent); TvApplication.setCurrentRunningProcess(context, true); if (!Features.TUNER.isEnabled(context)) { - enableTunerTvInputService(context, false, false, null); + enableTunerTvInputService(context, false); return; } - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context); switch (intent.getAction()) { case Intent.ACTION_BOOT_COMPLETED: - executeNetworkTunerDiscoveryAsyncTask(context, INITIAL_CHECKING_DURATION_MS); case TvApplication.ACTION_APPLICATION_FIRST_LAUNCHED: case UsbManager.ACTION_USB_DEVICE_ATTACHED: case UsbManager.ACTION_USB_DEVICE_DETACHED: - if (TunerHal.useBuiltInTuner(context)) { - enableTunerTvInputService(context, true, false, TunerHal.TUNER_TYPE_BUILT_IN); + if (TunerInputInfoUtils.isBuiltInTuner(context)) { + enableTunerTvInputService(context, true); break; } // Falls back to the below to check USB tuner devices. @@ -188,41 +123,7 @@ public class TunerInputController extends BroadcastReceiver { mHandler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context), DVB_DRIVER_CHECK_DELAY_MS); } else { - if (sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)) { - // Since network tuner is attached, do not disable TunerTvInput, - // just updates the TvInputInfo. - TunerInputInfoUtils.updateTunerInputInfo(context); - break; - } - enableTunerTvInputService(context, false, false, TextUtils - .equals(intent.getAction(), UsbManager.ACTION_USB_DEVICE_DETACHED) ? - TunerHal.TUNER_TYPE_USB : null); - } - break; - case CHECKING_NETWORK_CONNECTION: - long repeatedDurationMs = intent.getLongExtra(EXTRA_CHECKING_DURATION, - INITIAL_CHECKING_DURATION_MS); - executeNetworkTunerDiscoveryAsyncTask(context, - Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS)); - break; - case NETWORK_TUNER_ATTACHED: - // Network tuner detection is initiated by UI. So the app should not - // be killed. - sharedPreferences.edit() - .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, true).apply(); - enableTunerTvInputService(context, true, true, TunerHal.TUNER_TYPE_NETWORK); - break; - case NETWORK_TUNER_DETACHED: - sharedPreferences.edit() - .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false).apply(); - if(!isUsbTunerConnected(context) && !TunerHal.useBuiltInTuner(context)) { - // Network tuner detection is initiated by UI. So the app should not - // be killed. - enableTunerTvInputService(context, false, true, TunerHal.TUNER_TYPE_NETWORK); - } else { - // Since USB tuner is attached, do not disable TunerTvInput, - // just updates the TvInputInfo. - TunerInputInfoUtils.updateTunerInputInfo(context); + enableTunerTvInputService(context, false); } break; } @@ -237,15 +138,12 @@ public class TunerInputController extends BroadcastReceiver { private boolean isUsbTunerConnected(Context context) { UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); Map<String, UsbDevice> deviceList = manager.getDeviceList(); - String currentSecurityLevel = - SystemPropertiesProxy.getString(SECURITY_PATCH_LEVEL_KEY, null); - for (UsbDevice device : deviceList.values()) { if (DEBUG) { Log.d(TAG, "Device: " + device); } for (TunerDevice tuner : TUNER_DEVICES) { - if (tuner.equals(device) && tuner.isSupported(currentSecurityLevel)) { + if (tuner.equals(device)) { Log.i(TAG, "Tuner found"); return true; } @@ -260,8 +158,7 @@ public class TunerInputController extends BroadcastReceiver { * @param context {@link Context} instance * @param enabled {@code true} to enable the service; otherwise {@code false} */ - private void enableTunerTvInputService(Context context, boolean enabled, - boolean forceDontKillApp, Integer tunerType) { + private void enableTunerTvInputService(Context context, boolean enabled) { if (DEBUG) Log.d(TAG, "enableTunerTvInputService: " + enabled); PackageManager pm = context.getPackageManager(); ComponentName componentName = new ComponentName(context, TunerTvInputService.class); @@ -273,8 +170,7 @@ public class TunerInputController extends BroadcastReceiver { // Since PackageManager.DONT_KILL_APP delays the operation by 10 seconds // (PackageManagerService.BROADCAST_DELAY), we'd better avoid using it. It is used only // when the LiveChannels app is active since we don't want to kill the running app. - int flags = forceDontKillApp - || TvApplication.getSingletons(context).getMainActivityWrapper().isCreated() + int flags = TvApplication.getSingletons(context).getMainActivityWrapper().isCreated() ? PackageManager.DONT_KILL_APP : 0; int newState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; @@ -283,67 +179,14 @@ public class TunerInputController extends BroadcastReceiver { TunerSetupActivity.onTvInputEnabled(context, enabled); // Enable/disable the USB tuner TV input. pm.setComponentEnabledSetting(componentName, newState, flags); - if (!enabled && tunerType != null) { - if (tunerType == TunerHal.TUNER_TYPE_USB) { - Toast.makeText(context, R.string.msg_usb_tuner_disconnected, - Toast.LENGTH_SHORT).show(); - } else if (tunerType == TunerHal.TUNER_TYPE_NETWORK) { - Toast.makeText(context, R.string.msg_network_tuner_disconnected, - Toast.LENGTH_SHORT).show(); - } + if (!enabled) { + Toast.makeText( + context, R.string.msg_usb_device_detached, Toast.LENGTH_SHORT).show(); } if (DEBUG) Log.d(TAG, "Status updated:" + enabled); } else if (enabled) { - // When # of tuners is changed or the tuner input service is switching from/to using - // network tuners or the device just boots. + // When # of USB tuners is changed or the device just boots. TunerInputInfoUtils.updateTunerInputInfo(context); } } - - /** - * Discovers a network tuner. If the network connection is down, it won't repeatedly checking. - */ - public static void executeNetworkTunerDiscoveryAsyncTask(final Context context) { - executeNetworkTunerDiscoveryAsyncTask(context, 0); - } - - /** - * Discovers a network tuner. - * @param context {@link Context} - * @param repeatedDurationMs the time length to wait to repeatedly check network status to start - * finding network tuner when the network connection is not available. - * {@code 0} to disable repeatedly checking. - */ - private static void executeNetworkTunerDiscoveryAsyncTask(final Context context, - final long repeatedDurationMs) { - if (!Features.NETWORK_TUNER.isEnabled(context)) { - return; - } - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - if (isNetworkConnected(context)) { - // Implement and execute network tuner discovery AsyncTask here. - } else if (repeatedDurationMs > 0) { - AlarmManager alarmManager = - (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - Intent networkCheckingIntent = new Intent(context, TunerInputController.class); - networkCheckingIntent.setAction(CHECKING_NETWORK_CONNECTION); - networkCheckingIntent.putExtra(EXTRA_CHECKING_DURATION, repeatedDurationMs); - PendingIntent alarmIntent = PendingIntent.getBroadcast( - context, 0, networkCheckingIntent, PendingIntent.FLAG_UPDATE_CURRENT); - alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() - + repeatedDurationMs, alarmIntent); - } - return null; - } - }.execute(); - } - - private static boolean isNetworkConnected(Context context) { - ConnectivityManager cm = (ConnectivityManager) - context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = cm.getActiveNetworkInfo(); - return networkInfo != null && networkInfo.isConnected(); - } } diff --git a/src/com/android/tv/tuner/TunerPreferences.java b/src/com/android/tv/tuner/TunerPreferences.java index a387be74..1547e3ae 100644 --- a/src/com/android/tv/tuner/TunerPreferences.java +++ b/src/com/android/tv/tuner/TunerPreferences.java @@ -39,7 +39,6 @@ public class TunerPreferences { private static final String PREFS_KEY_CHANNEL_DATA_VERSION = "channel_data_version"; private static final String PREFS_KEY_SCANNED_CHANNEL_COUNT = "scanned_channel_count"; - private static final String PREFS_KEY_LAST_POSTAL_CODE = "last_postal_code"; private static final String PREFS_KEY_SCAN_DONE = "scan_done"; private static final String PREFS_KEY_LAUNCH_SETUP = "launch_setup"; private static final String PREFS_KEY_STORE_TS_STREAM = "store_ts_stream"; @@ -87,7 +86,8 @@ public class TunerPreferences { /** * Releases the resources. */ - public static synchronized void release(Context context) { + @MainThread + public static void release(Context context) { if (useContentProvider(context) && sContentObserver != null) { context.getContentResolver().unregisterContentObserver(sContentObserver); } @@ -99,8 +99,7 @@ public class TunerPreferences { * This preferences is used across processes, so the preferences should be loaded again when the * databases changes. */ - @MainThread - public static void loadPreferences(Context context) { + public static synchronized void loadPreferences(Context context) { if (sLoadPreferencesTask != null && sLoadPreferencesTask.getStatus() != AsyncTask.Status.FINISHED) { sLoadPreferencesTask.cancel(true); @@ -114,7 +113,8 @@ public class TunerPreferences { return TisConfiguration.isPackagedWithLiveChannels(context); } - public static synchronized int getChannelDataVersion(Context context) { + @MainThread + public static int getChannelDataVersion(Context context) { SoftPreconditions.checkState(sInitialized); if (useContentProvider(context)) { return sPreferenceValues.getInt(PREFS_KEY_CHANNEL_DATA_VERSION, @@ -126,7 +126,8 @@ public class TunerPreferences { } } - public static synchronized void setChannelDataVersion(Context context, int version) { + @MainThread + public static void setChannelDataVersion(Context context, int version) { if (useContentProvider(context)) { setPreference(context, PREFS_KEY_CHANNEL_DATA_VERSION, version); } else { @@ -136,7 +137,8 @@ public class TunerPreferences { } } - public static synchronized int getScannedChannelCount(Context context) { + @MainThread + public static int getScannedChannelCount(Context context) { SoftPreconditions.checkState(sInitialized); if (useContentProvider(context)) { return sPreferenceValues.getInt(PREFS_KEY_SCANNED_CHANNEL_COUNT); @@ -146,7 +148,8 @@ public class TunerPreferences { } } - public static synchronized void setScannedChannelCount(Context context, int channelCount) { + @MainThread + public static void setScannedChannelCount(Context context, int channelCount) { if (useContentProvider(context)) { setPreference(context, PREFS_KEY_SCANNED_CHANNEL_COUNT, channelCount); } else { @@ -156,25 +159,8 @@ public class TunerPreferences { } } - public static synchronized String getLastPostalCode(Context context) { - SoftPreconditions.checkState(sInitialized); - if (useContentProvider(context)) { - return sPreferenceValues.getString(PREFS_KEY_LAST_POSTAL_CODE); - } else { - return getSharedPreferences(context).getString(PREFS_KEY_LAST_POSTAL_CODE, null); - } - } - - public static synchronized void setLastPostalCode(Context context, String postalCode) { - if (useContentProvider(context)) { - setPreference(context, PREFS_KEY_LAST_POSTAL_CODE, postalCode); - } else { - getSharedPreferences(context).edit() - .putString(PREFS_KEY_LAST_POSTAL_CODE, postalCode).apply(); - } - } - - public static synchronized boolean isScanDone(Context context) { + @MainThread + public static boolean isScanDone(Context context) { SoftPreconditions.checkState(sInitialized); if (useContentProvider(context)) { return sPreferenceValues.getBoolean(PREFS_KEY_SCAN_DONE); @@ -184,7 +170,8 @@ public class TunerPreferences { } } - public static synchronized void setScanDone(Context context) { + @MainThread + public static void setScanDone(Context context) { if (useContentProvider(context)) { setPreference(context, PREFS_KEY_SCAN_DONE, true); } else { @@ -194,7 +181,8 @@ public class TunerPreferences { } } - public static synchronized boolean shouldShowSetupActivity(Context context) { + @MainThread + public static boolean shouldShowSetupActivity(Context context) { SoftPreconditions.checkState(sInitialized); if (useContentProvider(context)) { return sPreferenceValues.getBoolean(PREFS_KEY_LAUNCH_SETUP); @@ -204,7 +192,8 @@ public class TunerPreferences { } } - public static synchronized void setShouldShowSetupActivity(Context context, boolean need) { + @MainThread + public static void setShouldShowSetupActivity(Context context, boolean need) { if (useContentProvider(context)) { setPreference(context, PREFS_KEY_LAUNCH_SETUP, need); } else { @@ -214,7 +203,8 @@ public class TunerPreferences { } } - public static synchronized boolean getStoreTsStream(Context context) { + @MainThread + public static boolean getStoreTsStream(Context context) { SoftPreconditions.checkState(sInitialized); if (useContentProvider(context)) { return sPreferenceValues.getBoolean(PREFS_KEY_STORE_TS_STREAM, false); @@ -224,7 +214,8 @@ public class TunerPreferences { } } - public static synchronized void setStoreTsStream(Context context, boolean shouldStore) { + @MainThread + public static void setStoreTsStream(Context context, boolean shouldStore) { if (useContentProvider(context)) { setPreference(context, PREFS_KEY_STORE_TS_STREAM, shouldStore); } else { @@ -238,23 +229,8 @@ public class TunerPreferences { return context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); } - private static synchronized void setPreference(Context context, String key, String value) { - sPreferenceValues.putString(key, value); - savePreference(context, key, value); - } - - private static synchronized void setPreference(Context context, String key, int value) { - sPreferenceValues.putInt(key, value); - savePreference(context, key, Integer.toString(value)); - } - - private static synchronized void setPreference(Context context, String key, boolean value) { - sPreferenceValues.putBoolean(key, value); - savePreference(context, key, Boolean.toString(value)); - } - - private static void savePreference(final Context context, final String key, - final String value) { + @MainThread + private static void setPreference(final Context context, final String key, final String value) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { @@ -273,6 +249,18 @@ public class TunerPreferences { }.execute(); } + @MainThread + private static void setPreference(Context context, String key, int value) { + sPreferenceValues.putInt(key, value); + setPreference(context, key, Integer.toString(value)); + } + + @MainThread + private static void setPreference(Context context, String key, boolean value) { + sPreferenceValues.putBoolean(key, value); + setPreference(context, key, Boolean.toString(value)); + } + private static class LoadPreferencesTask extends AsyncTask<Void, Void, Bundle> { private final Context mContext; private LoadPreferencesTask(Context context) { @@ -304,9 +292,6 @@ public class TunerPreferences { case PREFS_KEY_STORE_TS_STREAM: bundle.putBoolean(key, Boolean.parseBoolean(value)); break; - case PREFS_KEY_LAST_POSTAL_CODE: - bundle.putString(key, value); - break; } } } @@ -318,10 +303,8 @@ public class TunerPreferences { } @Override - protected synchronized void onPostExecute(Bundle bundle) { - if (bundle != null) { - sPreferenceValues.putAll(bundle); - } + protected void onPostExecute(Bundle bundle) { + sPreferenceValues.putAll(bundle); } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/tuner/UsbTunerHal.java b/src/com/android/tv/tuner/UsbTunerHal.java index b1608ede..22e35ea1 100644 --- a/src/com/android/tv/tuner/UsbTunerHal.java +++ b/src/com/android/tv/tuner/UsbTunerHal.java @@ -169,10 +169,6 @@ public class UsbTunerHal extends TunerHal { * Gets the number of USB tuner devices currently present. */ public static int getNumberOfDevices(Context context) { - try { - return (new DvbDeviceAccessor(context)).getNumOfDvbDevices(); - } catch (Exception e) { - return 0; - } + return (new DvbDeviceAccessor(context)).getNumOfDvbDevices(); } } diff --git a/src/com/android/tv/tuner/cc/CaptionLayout.java b/src/com/android/tv/tuner/cc/CaptionLayout.java index c41f1014..a88538df 100644 --- a/src/com/android/tv/tuner/cc/CaptionLayout.java +++ b/src/com/android/tv/tuner/cc/CaptionLayout.java @@ -19,7 +19,7 @@ package com.android.tv.tuner.cc; import android.content.Context; import android.util.AttributeSet; -import com.android.tv.tuner.data.Track.AtscCaptionTrack; +import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; import com.android.tv.tuner.layout.ScaledLayout; /** diff --git a/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java b/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java index 3aa40982..3c75caa9 100644 --- a/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java +++ b/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java @@ -27,7 +27,7 @@ import com.android.tv.tuner.data.Cea708Data.CaptionPenColor; import com.android.tv.tuner.data.Cea708Data.CaptionPenLocation; import com.android.tv.tuner.data.Cea708Data.CaptionWindow; import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr; -import com.android.tv.tuner.data.Track.AtscCaptionTrack; +import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; import java.util.ArrayList; import java.util.concurrent.TimeUnit; diff --git a/src/com/android/tv/tuner/cc/Cea708Parser.java b/src/com/android/tv/tuner/cc/Cea708Parser.java index c43fe512..92ab0620 100644 --- a/src/com/android/tv/tuner/cc/Cea708Parser.java +++ b/src/com/android/tv/tuner/cc/Cea708Parser.java @@ -140,7 +140,6 @@ public class Cea708Parser { private int mCommand = 0; private int mListenServiceNumber = 0; private boolean mDtvCcPacking = false; - private boolean mFirstServiceNumberDiscovered; // Assign a dummy listener in order to avoid null checks. private OnCea708ParserListener mListener = new OnCea708ParserListener() { @@ -333,14 +332,12 @@ public class Cea708Parser { mDiscoveredNumBytes.put( serviceNumber, blockSize + mDiscoveredNumBytes.get(serviceNumber, 0)); } - if (mLastDiscoveryLaunchedMs + DISCOVERY_PERIOD_MS < SystemClock.elapsedRealtime() - || !mFirstServiceNumberDiscovered) { + if (mLastDiscoveryLaunchedMs + DISCOVERY_PERIOD_MS < SystemClock.elapsedRealtime()) { for (int i = 0; i < mDiscoveredNumBytes.size(); ++i) { int discoveredNumBytes = mDiscoveredNumBytes.valueAt(i); if (discoveredNumBytes >= DISCOVERY_NUM_BYTES_THRESHOLD) { int discoveredServiceNumber = mDiscoveredNumBytes.keyAt(i); mListener.discoverServiceNumber(discoveredServiceNumber); - mFirstServiceNumberDiscovered = true; } } mDiscoveredNumBytes.clear(); diff --git a/src/com/android/tv/tuner/data/PsiData.java b/src/com/android/tv/tuner/data/PsiData.java index 2c8a52db..67700c6a 100644 --- a/src/com/android/tv/tuner/data/PsiData.java +++ b/src/com/android/tv/tuner/data/PsiData.java @@ -17,8 +17,8 @@ package com.android.tv.tuner.data; -import com.android.tv.tuner.data.Track.AtscAudioTrack; -import com.android.tv.tuner.data.Track.AtscCaptionTrack; +import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; +import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; import java.util.List; diff --git a/src/com/android/tv/tuner/data/PsipData.java b/src/com/android/tv/tuner/data/PsipData.java index ac7fdedb..aead4be8 100644 --- a/src/com/android/tv/tuner/data/PsipData.java +++ b/src/com/android/tv/tuner/data/PsipData.java @@ -20,11 +20,11 @@ import android.support.annotation.NonNull; import android.text.TextUtils; import android.text.format.DateUtils; -import com.android.tv.tuner.data.Track.AtscAudioTrack; -import com.android.tv.tuner.data.Track.AtscCaptionTrack; +import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; +import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; import com.android.tv.tuner.ts.SectionParser; import com.android.tv.tuner.util.ConvertUtils; -import com.android.tv.util.StringUtils; +import com.android.tv.tuner.util.StringUtils; import java.util.ArrayList; import java.util.List; diff --git a/src/com/android/tv/tuner/data/TunerChannel.java b/src/com/android/tv/tuner/data/TunerChannel.java index 41f66e7d..89079d77 100644 --- a/src/com/android/tv/tuner/data/TunerChannel.java +++ b/src/com/android/tv/tuner/data/TunerChannel.java @@ -19,11 +19,12 @@ package com.android.tv.tuner.data; import android.support.annotation.NonNull; import android.util.Log; -import com.android.tv.tuner.data.Channel.TunerChannelProto; -import com.android.tv.tuner.data.Track.AtscAudioTrack; -import com.android.tv.tuner.data.Track.AtscCaptionTrack; +import com.android.tv.tuner.data.nano.Channel; +import com.android.tv.tuner.data.nano.Channel.TunerChannelProto; +import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; +import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; import com.android.tv.tuner.util.Ints; -import com.android.tv.util.StringUtils; +import com.android.tv.tuner.util.StringUtils; import com.google.protobuf.nano.MessageNano; import java.io.IOException; @@ -39,11 +40,6 @@ import java.util.Objects; public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracksInterface { private static final String TAG = "TunerChannel"; - /** - * Channel number separator between major number and minor number. - */ - public static final char CHANNEL_NUMBER_SEPARATOR = '-'; - // See ATSC Code Points Registry. private static final String[] ATSC_SERVICE_TYPE_NAMES = new String[] { "ATSC Reserved", @@ -67,7 +63,6 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks // According to ISO13818-1, Mpeg2 StreamType has a range from 0x00 to 0xff. public static final int INVALID_STREAMTYPE = -1; - // @GuardedBy(this) Writing operations and toByteArray will be guarded. b/34197766 private final TunerChannelProto mProto; private TunerChannel(PsipData.VctItem channel, int programNumber, @@ -150,44 +145,6 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks return new TunerChannel(channel, 0, pmtItems, Channel.TYPE_FILE); } - /** - * Create a TunerChannel object suitable for network tuners - * @param major Channel number major - * @param minor Channel number minor - * @param programNumber Program number - * @param shortName Short name - * @param recordingProhibited Recording prohibition info - * @param videoFormat Video format. Should be {@code null} or one of the followings: - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_240P}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_360P}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_480I}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_480P}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_576I}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_576P}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_720P}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_1080I}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_1080P}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_2160P}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_4320P} - * @return a TunerChannel object - */ - public static TunerChannel forNetwork(int major, int minor, int programNumber, - String shortName, boolean recordingProhibited, String videoFormat) { - TunerChannel tunerChannel = new TunerChannel(programNumber, Collections.EMPTY_LIST); - tunerChannel.setVirtualMajor(major); - tunerChannel.setVirtualMinor(minor); - tunerChannel.setShortName(shortName); - // Set audio and video pids in order to work around the audio-only channel check. - tunerChannel.setAudioPids(new ArrayList<>(Arrays.asList(0))); - tunerChannel.selectAudioTrack(0); - tunerChannel.setVideoPid(0); - tunerChannel.setRecordingProhibited(recordingProhibited); - if (videoFormat != null) { - tunerChannel.setVideoFormat(videoFormat); - } - return tunerChannel; - } - public String getName() { return (!mProto.shortName.isEmpty()) ? mProto.shortName : mProto.longName; } @@ -236,7 +193,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks return mProto.videoPid; } - synchronized public void setVideoPid(int videoPid) { + public void setVideoPid(int videoPid) { mProto.videoPid = videoPid; } @@ -262,7 +219,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks return Ints.asList(mProto.audioPids); } - synchronized public void setAudioPids(List<Integer> audioPids) { + public void setAudioPids(List<Integer> audioPids) { mProto.audioPids = Ints.toArray(audioPids); } @@ -270,7 +227,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks return Ints.asList(mProto.audioStreamTypes); } - synchronized public void setAudioStreamTypes(List<Integer> audioStreamTypes) { + public void setAudioStreamTypes(List<Integer> audioStreamTypes) { mProto.audioStreamTypes = Ints.toArray(audioStreamTypes); } @@ -282,32 +239,32 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks return mProto.type; } - synchronized public void setFilepath(String filepath) { - mProto.filepath = filepath == null ? "" : filepath; + public void setFilepath(String filepath) { + mProto.filepath = filepath; } public String getFilepath() { return mProto.filepath; } - synchronized public void setVirtualMajor(int virtualMajor) { + public void setVirtualMajor(int virtualMajor) { mProto.virtualMajor = virtualMajor; } - synchronized public void setVirtualMinor(int virtualMinor) { + public void setVirtualMinor(int virtualMinor) { mProto.virtualMinor = virtualMinor; } - synchronized public void setShortName(String shortName) { - mProto.shortName = shortName == null ? "" : shortName; + public void setShortName(String shortName) { + mProto.shortName = shortName; } - synchronized public void setFrequency(int frequency) { + public void setFrequency(int frequency) { mProto.frequency = frequency; } - synchronized public void setModulation(String modulation) { - mProto.modulation = modulation == null ? "" : modulation; + public void setModulation(String modulation) { + mProto.modulation = modulation; } public boolean hasVideo() { @@ -322,18 +279,13 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks return mProto.channelId; } - synchronized public void setChannelId(long channelId) { + public void setChannelId(long channelId) { mProto.channelId = channelId; } public String getDisplayNumber() { - return getDisplayNumber(true); - } - - public String getDisplayNumber(boolean ignoreZeroMinorNumber) { - if (mProto.virtualMajor != 0 && (mProto.virtualMinor != 0 || !ignoreZeroMinorNumber)) { - return String.format("%d%c%d", mProto.virtualMajor, CHANNEL_NUMBER_SEPARATOR, - mProto.virtualMinor); + if (mProto.virtualMajor != 0 && mProto.virtualMinor != 0) { + return String.format("%d-%d", mProto.virtualMajor, mProto.virtualMinor); } else if (mProto.virtualMajor != 0) { return Integer.toString(mProto.virtualMajor); } else { @@ -346,7 +298,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks } @Override - synchronized public void setHasCaptionTrack() { + public void setHasCaptionTrack() { mProto.hasCaptionTrack = true; } @@ -360,7 +312,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks return Collections.unmodifiableList(Arrays.asList(mProto.audioTracks)); } - synchronized public void setAudioTracks(List<AtscAudioTrack> audioTracks) { + public void setAudioTracks(List<AtscAudioTrack> audioTracks) { mProto.audioTracks = audioTracks.toArray(new AtscAudioTrack[audioTracks.size()]); } @@ -369,11 +321,11 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks return Collections.unmodifiableList(Arrays.asList(mProto.captionTracks)); } - synchronized public void setCaptionTracks(List<AtscCaptionTrack> captionTracks) { + public void setCaptionTracks(List<AtscCaptionTrack> captionTracks) { mProto.captionTracks = captionTracks.toArray(new AtscCaptionTrack[captionTracks.size()]); } - synchronized public void selectAudioTrack(int index) { + public void selectAudioTrack(int index) { if (0 <= index && index < mProto.audioPids.length) { mProto.audioTrackIndex = index; } else { @@ -381,22 +333,6 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks } } - synchronized public void setRecordingProhibited(boolean recordingProhibited) { - mProto.recordingProhibited = recordingProhibited; - } - - public boolean isRecordingProhibited() { - return mProto.recordingProhibited; - } - - synchronized public void setVideoFormat(String videoFormat) { - mProto.videoFormat = videoFormat == null ? "" : videoFormat; - } - - public String getVideoFormat() { - return mProto.videoFormat; - } - @Override public String toString() { switch (mProto.type) { @@ -423,10 +359,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks if (ret != 0) { return ret; } - ret = StringUtils.compare(getName(), channel.getName()); - if (ret != 0) { - return ret; - } + // For FileTsStreamer, file paths should be compared. return StringUtils.compare(getFilepath(), channel.getFilepath()); } @@ -441,19 +374,12 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks @Override public int hashCode() { - return Objects.hash(getFrequency(), getProgramNumber(), getName(), getFilepath()); + return Objects.hash(getFrequency(), getProgramNumber(), getFilepath()); } // Serialization - synchronized public byte[] toByteArray() { - try { - return MessageNano.toByteArray(mProto); - } catch (Exception e) { - // Retry toByteArray. b/34197766 - Log.w(TAG, "TunerChannel or its variables are modified in multiple thread without lock", - e); - return MessageNano.toByteArray(mProto); - } + public byte[] toByteArray() { + return MessageNano.toByteArray(mProto); } public static TunerChannel parseFrom(byte[] data) { diff --git a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java index 89641530..c105e222 100644 --- a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java +++ b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java @@ -23,29 +23,17 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.SystemClock; -import android.util.Pair; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.extractor.ExtractorSampleSource; +import com.google.android.exoplayer.extractor.ExtractorSampleSource.EventListener; +import com.google.android.exoplayer.upstream.Allocator; import com.google.android.exoplayer.upstream.DataSource; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener; -import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.SampleStream; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.FixedTrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.DefaultAllocator; -import com.android.tv.tuner.exoplayer.ac3.Ac3DefaultTrackRenderer; +import com.google.android.exoplayer.upstream.DefaultAllocator; import com.android.tv.tuner.exoplayer.buffer.BufferManager; import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer; import com.android.tv.tuner.exoplayer.buffer.SimpleSampleBuffer; @@ -54,11 +42,10 @@ import com.android.tv.tuner.tvinput.PlaybackBufferListener; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; /** * A class that extracts samples from a live broadcast stream while storing the sample on the disk. @@ -67,7 +54,11 @@ import java.util.concurrent.atomic.AtomicBoolean; public class ExoPlayerSampleExtractor implements SampleExtractor { private static final String TAG = "ExoPlayerSampleExtracto"; - private static final int INVALID_TRACK_INDEX = -1; + // Buffer segment size for memory allocator. Copied from demo implementation of ExoPlayer. + private static final int BUFFER_SEGMENT_SIZE_IN_BYTES = 64 * 1024; + // Buffer segment count for sample source. Copied from demo implementation of ExoPlayer. + private static final int BUFFER_SEGMENT_COUNT = 256; + private final HandlerThread mSourceReaderThread; private final long mId; @@ -79,69 +70,36 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { private AtomicBoolean mOnCompletionCalled = new AtomicBoolean(); private IOException mExceptionOnPrepare; private List<MediaFormat> mTrackFormats; - private int mVideoTrackIndex = INVALID_TRACK_INDEX; - private boolean mVideoTrackMet; - private long mBaseSamplePts = Long.MIN_VALUE; private HashMap<Integer, Long> mLastExtractedPositionUsMap = new HashMap<>(); - private final List<Pair<Integer, SampleHolder>> mPendingSamples = new LinkedList<>(); private OnCompletionListener mOnCompletionListener; private Handler mOnCompletionListenerHandler; private IOException mError; - public ExoPlayerSampleExtractor(Uri uri, final DataSource source, BufferManager bufferManager, + public ExoPlayerSampleExtractor(Uri uri, DataSource source, BufferManager bufferManager, PlaybackBufferListener bufferListener, boolean isRecording) { // It'll be used as a timeshift file chunk name's prefix. mId = System.currentTimeMillis(); + Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE_IN_BYTES); EventListener eventListener = new EventListener() { + @Override - public void onLoadError(IOException error) { - mError = error; + public void onLoadError(int sourceId, IOException e) { + mError = e; } }; mSourceReaderThread = new HandlerThread("SourceReaderThread"); - mSourceReaderWorker = new SourceReaderWorker(new ExtractorMediaSource(uri, - new com.google.android.exoplayer2.upstream.DataSource.Factory() { - @Override - public com.google.android.exoplayer2.upstream.DataSource createDataSource() { - // Returns an adapter implementation for ExoPlayer V2 DataSource interface. - return new com.google.android.exoplayer2.upstream.DataSource() { - @Override - public long open(DataSpec dataSpec) throws IOException { - return source.open( - new com.google.android.exoplayer.upstream.DataSpec( - dataSpec.uri, dataSpec.postBody, - dataSpec.absoluteStreamPosition, dataSpec.position, - dataSpec.length, dataSpec.key, dataSpec.flags)); - } - - @Override - public int read(byte[] buffer, int offset, int readLength) - throws IOException { - return source.read(buffer, offset, readLength); - } - - @Override - public Uri getUri() { - return null; - } - - @Override - public void close() throws IOException { - source.close(); - } - }; - } - }, - new DefaultExtractorsFactory(), + mSourceReaderWorker = new SourceReaderWorker(new ExtractorSampleSource(uri, source, + allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE_IN_BYTES, // Do not create a handler if we not on a looper. e.g. test. - Looper.myLooper() != null ? new Handler() : null, eventListener)); + Looper.myLooper() != null ? new Handler() : null, + eventListener, 0)); if (isRecording) { mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, false, RecordingSampleBuffer.BUFFER_REASON_RECORDING); } else { - if (bufferManager == null) { + if (bufferManager == null || bufferManager.isDisabled()) { mSampleBuffer = new SimpleSampleBuffer(bufferListener); } else { mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, true, @@ -156,141 +114,43 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { mOnCompletionListenerHandler = handler; } - private class SourceReaderWorker implements Handler.Callback, MediaPeriod.Callback { + private class SourceReaderWorker implements Handler.Callback { public static final int MSG_PREPARE = 1; public static final int MSG_FETCH_SAMPLES = 2; public static final int MSG_RELEASE = 3; private static final int RETRY_INTERVAL_MS = 50; - private final MediaSource mSampleSource; - private MediaPeriod mMediaPeriod; - private SampleStream[] mStreams; + private final SampleSource mSampleSource; + private SampleSource.SampleSourceReader mSampleSourceReader; private boolean[] mTrackMetEos; private boolean mMetEos = false; private long mCurrentPosition; - private DecoderInputBuffer mDecoderInputBuffer; - private SampleHolder mSampleHolder; - private boolean mPrepareRequested; - public SourceReaderWorker(MediaSource sampleSource) { + public SourceReaderWorker(SampleSource sampleSource) { mSampleSource = sampleSource; - mSampleSource.prepareSource(null, false, new MediaSource.Listener() { - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - // Dynamic stream change is not supported yet. b/28169263 - // For now, this will cause EOS and playback reset. - } - }); - mDecoderInputBuffer = new DecoderInputBuffer( - DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); - mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); - } - - MediaFormat convertFormat(Format format) { - if (format.sampleMimeType.startsWith("audio/")) { - return MediaFormat.createAudioFormat(format.id, format.sampleMimeType, - format.bitrate, format.maxInputSize, - com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.channelCount, - format.sampleRate, format.initializationData, format.language, - format.pcmEncoding); - } else if (format.sampleMimeType.startsWith("video/")) { - return MediaFormat.createVideoFormat( - format.id, format.sampleMimeType, format.bitrate, format.maxInputSize, - com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.width, format.height, - format.initializationData, format.rotationDegrees, - format.pixelWidthHeightRatio, format.projectionData, format.stereoMode); - } else if (format.sampleMimeType.endsWith("/cea-608") - || format.sampleMimeType.startsWith("text/")) { - return MediaFormat.createTextFormat( - format.id, format.sampleMimeType, format.bitrate, - com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.language); - } else { - return MediaFormat.createFormatForMimeType( - format.id, format.sampleMimeType, format.bitrate, - com.google.android.exoplayer.C.UNKNOWN_TIME_US); - } - } - - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - if (mMediaPeriod == null) { - // This instance is already released while the extractor is preparing. - return; - } - TrackSelection.Factory selectionFactory = new FixedTrackSelection.Factory(); - TrackGroupArray trackGroupArray = mMediaPeriod.getTrackGroups(); - TrackSelection[] selections = new TrackSelection[trackGroupArray.length]; - for (int i = 0; i < selections.length; ++i) { - selections[i] = selectionFactory.createTrackSelection(trackGroupArray.get(i), 0); - } - boolean retain[] = new boolean[trackGroupArray.length]; - boolean reset[] = new boolean[trackGroupArray.length]; - mStreams = new SampleStream[trackGroupArray.length]; - mMediaPeriod.selectTracks(selections, retain, mStreams, reset, 0); - if (mTrackFormats == null) { - int trackCount = trackGroupArray.length; - mTrackMetEos = new boolean[trackCount]; - List<MediaFormat> trackFormats = new ArrayList<>(); - int videoTrackCount = 0; - for (int i = 0; i < trackCount; i++) { - Format format = trackGroupArray.get(i).getFormat(0); - if (format.sampleMimeType.startsWith("video/")) { - videoTrackCount++; - mVideoTrackIndex = i; - } - trackFormats.add(convertFormat(format)); - } - if (videoTrackCount > 1) { - // Disable dropping samples when there are multiple video tracks. - mVideoTrackIndex = INVALID_TRACK_INDEX; - } - mTrackFormats = trackFormats; - List<String> ids = new ArrayList<>(); - for (int i = 0; i < mTrackFormats.size(); i++) { - ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i)); - } - try { - mSampleBuffer.init(ids, mTrackFormats); - } catch (IOException e) { - // In this case, we will not schedule any further operation. - // mExceptionOnPrepare will be notified to ExoPlayer, and ExoPlayer will - // call release() eventually. - mExceptionOnPrepare = e; - return; - } - mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); - mPrepared = true; - } - } - - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - source.continueLoading(mCurrentPosition); } @Override public boolean handleMessage(Message message) { switch (message.what) { case MSG_PREPARE: - if (!mPrepareRequested) { - mPrepareRequested = true; - mMediaPeriod = mSampleSource.createPeriod(0, - new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE), 0); - mMediaPeriod.prepare(this); - try { - mMediaPeriod.maybeThrowPrepareError(); - } catch (IOException e) { - mError = e; - } + mPrepared = prepare(); + if (!mPrepared && mExceptionOnPrepare == null) { + mSourceReaderHandler + .sendEmptyMessageDelayed(MSG_PREPARE, RETRY_INTERVAL_MS); + } else{ + mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); } return true; case MSG_FETCH_SAMPLES: boolean didSomething = false; + SampleHolder sample = new SampleHolder( + SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); ConditionVariable conditionVariable = new ConditionVariable(); - int trackCount = mStreams.length; + int trackCount = mSampleSourceReader.getTrackCount(); for (int i = 0; i < trackCount; ++i) { - if (!mTrackMetEos[i] && C.RESULT_NOTHING_READ - != fetchSample(i, mSampleHolder, conditionVariable)) { + if (!mTrackMetEos[i] && SampleSource.NOTHING_READ + != fetchSample(i, sample, conditionVariable)) { if (mMetEos) { // If mMetEos was on during fetchSample() due to an error, // fetching from other tracks is not necessary. @@ -299,7 +159,6 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { didSomething = true; } } - mMediaPeriod.continueLoading(mCurrentPosition); if (!mMetEos) { if (didSomething) { mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); @@ -312,10 +171,17 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { } return true; case MSG_RELEASE: - if (mMediaPeriod != null) { - mSampleSource.releasePeriod(mMediaPeriod); - mSampleSource.releaseSource(); - mMediaPeriod = null; + if (mSampleSourceReader != null) { + if (mPrepared) { + // ExtractorSampleSource expects all the tracks should be disabled + // before releasing. + int count = mSampleSourceReader.getTrackCount(); + for (int i = 0; i < count; ++i) { + mSampleSourceReader.disable(i); + } + } + mSampleSourceReader.release(); + mSampleSourceReader = null; } cleanUp(); mSourceReaderHandler.removeCallbacksAndMessages(null); @@ -324,109 +190,91 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { return false; } + private boolean prepare() { + if (mSampleSourceReader == null) { + mSampleSourceReader = mSampleSource.register(); + } + if(!mSampleSourceReader.prepare(0)) { + return false; + } + if (mTrackFormats == null) { + int trackCount = mSampleSourceReader.getTrackCount(); + mTrackMetEos = new boolean[trackCount]; + List<MediaFormat> trackFormats = new ArrayList<>(); + for (int i = 0; i < trackCount; i++) { + trackFormats.add(mSampleSourceReader.getFormat(i)); + mSampleSourceReader.enable(i, 0); + + } + mTrackFormats = trackFormats; + List<String> ids = new ArrayList<>(); + for (int i = 0; i < mTrackFormats.size(); i++) { + ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i)); + } + try { + mSampleBuffer.init(ids, mTrackFormats); + } catch (IOException e) { + // In this case, we will not schedule any further operation. + // mExceptionOnPrepare will be notified to ExoPlayer, and ExoPlayer will + // call release() eventually. + mExceptionOnPrepare = e; + return false; + } + } + return true; + } + private int fetchSample(int track, SampleHolder sample, ConditionVariable conditionVariable) { - FormatHolder dummyFormatHolder = new FormatHolder(); - mDecoderInputBuffer.clear(); - int ret = mStreams[track].readData(dummyFormatHolder, mDecoderInputBuffer); - if (ret == C.RESULT_BUFFER_READ - // Double-check if the extractor provided the data to prevent NPE. b/33758354 - && mDecoderInputBuffer.data != null) { - if (mCurrentPosition < mDecoderInputBuffer.timeUs) { - mCurrentPosition = mDecoderInputBuffer.timeUs; + mSampleSourceReader.continueBuffering(track, mCurrentPosition); + + MediaFormatHolder formatHolder = new MediaFormatHolder(); + sample.clearData(); + int ret = mSampleSourceReader.readData(track, mCurrentPosition, formatHolder, sample); + if (ret == SampleSource.SAMPLE_READ) { + if (mCurrentPosition < sample.timeUs) { + mCurrentPosition = sample.timeUs; } try { Long lastExtractedPositionUs = mLastExtractedPositionUsMap.get(track); if (lastExtractedPositionUs == null) { - mLastExtractedPositionUsMap.put(track, mDecoderInputBuffer.timeUs); + mLastExtractedPositionUsMap.put(track, sample.timeUs); } else { mLastExtractedPositionUsMap.put(track, - Math.max(lastExtractedPositionUs, mDecoderInputBuffer.timeUs)); + Math.max(lastExtractedPositionUs, sample.timeUs)); } - queueSample(track, conditionVariable); + queueSample(track, sample, conditionVariable); } catch (IOException e) { mLastExtractedPositionUsMap.clear(); mMetEos = true; mSampleBuffer.setEos(); } - } else if (ret == C.RESULT_END_OF_INPUT) { + } else if (ret == SampleSource.END_OF_STREAM) { mTrackMetEos[track] = true; for (int i = 0; i < mTrackMetEos.length; ++i) { if (!mTrackMetEos[i]) { break; } - if (i == mTrackMetEos.length - 1) { + if (i == mTrackMetEos.length -1) { mMetEos = true; mSampleBuffer.setEos(); } } } - // TODO: Handle C.RESULT_FORMAT_READ for dynamic resolution change. b/28169263 + // TODO: Handle SampleSource.FORMAT_READ for dynamic resolution change. b/28169263 return ret; } + } - private void queueSample(int index, ConditionVariable conditionVariable) - throws IOException { - if (mVideoTrackIndex != INVALID_TRACK_INDEX) { - if (!mVideoTrackMet) { - if (index != mVideoTrackIndex) { - SampleHolder sample = - new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); - mSampleHolder.flags = - (mDecoderInputBuffer.isKeyFrame() - ? com.google.android.exoplayer.C.SAMPLE_FLAG_SYNC : 0) - | (mDecoderInputBuffer.isDecodeOnly() - ? com.google.android.exoplayer.C.SAMPLE_FLAG_DECODE_ONLY - : 0); - sample.timeUs = mDecoderInputBuffer.timeUs; - sample.size = mDecoderInputBuffer.data.position(); - sample.ensureSpaceForWrite(sample.size); - mDecoderInputBuffer.flip(); - sample.data.position(0); - sample.data.put(mDecoderInputBuffer.data); - sample.data.flip(); - mPendingSamples.add(new Pair<>(index, sample)); - return; - } - mVideoTrackMet = true; - mBaseSamplePts = - mDecoderInputBuffer.timeUs - - Ac3DefaultTrackRenderer.INITIAL_AUDIO_BUFFERING_TIME_US; - for (Pair<Integer, SampleHolder> pair : mPendingSamples) { - if (pair.second.timeUs >= mBaseSamplePts) { - mSampleBuffer.writeSample(pair.first, pair.second, conditionVariable); - } - } - mPendingSamples.clear(); - } else { - if (mDecoderInputBuffer.timeUs < mBaseSamplePts - && mVideoTrackIndex != index) { - return; - } - } - } - // Copy the decoder input to the sample holder. - mSampleHolder.clearData(); - mSampleHolder.flags = - (mDecoderInputBuffer.isKeyFrame() - ? com.google.android.exoplayer.C.SAMPLE_FLAG_SYNC : 0) - | (mDecoderInputBuffer.isDecodeOnly() - ? com.google.android.exoplayer.C.SAMPLE_FLAG_DECODE_ONLY : 0); - mSampleHolder.timeUs = mDecoderInputBuffer.timeUs; - mSampleHolder.size = mDecoderInputBuffer.data.position(); - mSampleHolder.ensureSpaceForWrite(mSampleHolder.size); - mDecoderInputBuffer.flip(); - mSampleHolder.data.position(0); - mSampleHolder.data.put(mDecoderInputBuffer.data); - mSampleHolder.data.flip(); - long writeStartTimeNs = SystemClock.elapsedRealtimeNanos(); - mSampleBuffer.writeSample(index, mSampleHolder, conditionVariable); - - // Checks whether the storage has enough bandwidth for recording samples. - if (mSampleBuffer.isWriteSpeedSlow(mSampleHolder.size, - SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) { - mSampleBuffer.handleWriteSpeedSlow(); - } + private void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable) + throws IOException { + long writeStartTimeNs = SystemClock.elapsedRealtimeNanos(); + mSampleBuffer.writeSample(index, sample, conditionVariable); + + // Checks whether the storage has enough bandwidth for recording samples. + if (mSampleBuffer.isWriteSpeedSlow(sample.size, + SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) { + mSampleBuffer.handleWriteSpeedSlow(); } } @@ -480,7 +328,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { } @Override - public boolean continueBuffering(long positionUs) { + public boolean continueBuffering(long positionUs) { return mSampleBuffer.continueBuffering(positionUs); } @@ -538,14 +386,12 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { } private long getLastExtractedPositionUs() { - long lastExtractedPositionUs = Long.MIN_VALUE; - for (Map.Entry<Integer, Long> entry : mLastExtractedPositionUsMap.entrySet()) { - if (mVideoTrackIndex != entry.getKey()) { - lastExtractedPositionUs = Math.max(lastExtractedPositionUs, entry.getValue()); - } + long lastExtractedPositionUs = Long.MAX_VALUE; + for (long value : mLastExtractedPositionUsMap.values()) { + lastExtractedPositionUs = Math.min(lastExtractedPositionUs, value); } - if (lastExtractedPositionUs == Long.MIN_VALUE) { - lastExtractedPositionUs = com.google.android.exoplayer.C.UNKNOWN_TIME_US; + if (lastExtractedPositionUs == Long.MAX_VALUE) { + lastExtractedPositionUs = C.UNKNOWN_TIME_US; } return lastExtractedPositionUs; } diff --git a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java index b7e42a7c..ec7b4b16 100644 --- a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java +++ b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java @@ -25,6 +25,7 @@ import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer; import com.android.tv.tuner.tvinput.PlaybackBufferListener; import android.os.Handler; +import android.util.Pair; import java.io.IOException; import java.util.ArrayList; @@ -60,17 +61,18 @@ public class FileSampleExtractor implements SampleExtractor{ @Override public boolean prepare() throws IOException { - List<BufferManager.TrackFormat> trackFormatList = mBufferManager.readTrackInfoFiles(); - if (trackFormatList == null || trackFormatList.isEmpty()) { + ArrayList<Pair<String, android.media.MediaFormat>> trackInfos = + mBufferManager.readTrackInfoFiles(); + if (trackInfos == null || trackInfos.isEmpty()) { throw new IOException("Cannot find meta files for the recording."); } - mTrackCount = trackFormatList.size(); + mTrackCount = trackInfos.size(); List<String> ids = new ArrayList<>(); mTrackFormats.clear(); for (int i = 0; i < mTrackCount; ++i) { - BufferManager.TrackFormat trackFormat = trackFormatList.get(i); - ids.add(trackFormat.trackId); - mTrackFormats.add(MediaFormatUtil.createMediaFormat(trackFormat.format)); + Pair<String, android.media.MediaFormat> pair = trackInfos.get(i); + ids.add(pair.first); + mTrackFormats.add(MediaFormatUtil.createMediaFormat(pair.second)); } mSampleBuffer = new RecordingSampleBuffer(mBufferManager, mBufferListener, true, RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK); diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java index ba0edf20..381b22e9 100644 --- a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java +++ b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java @@ -39,8 +39,8 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.tuner.data.Cea708Data; import com.android.tv.tuner.data.Cea708Data.CaptionEvent; import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.exoplayer.ac3.Ac3DefaultTrackRenderer; -import com.android.tv.tuner.exoplayer.ac3.Ac3MediaCodecTrackRenderer; +import com.android.tv.tuner.exoplayer.ac3.Ac3PassthroughTrackRenderer; +import com.android.tv.tuner.exoplayer.ac3.Ac3TrackRenderer; import com.android.tv.tuner.source.TsDataSource; import com.android.tv.tuner.source.TsDataSourceManager; import com.android.tv.tuner.tvinput.EventDetector; @@ -48,12 +48,11 @@ import com.android.tv.tuner.tvinput.EventDetector; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -/** MPEG-2 TS stream player implementation using ExoPlayer. */ -public class MpegTsPlayer - implements ExoPlayer.Listener, - MediaCodecVideoTrackRenderer.EventListener, - Ac3DefaultTrackRenderer.EventListener, - Ac3MediaCodecTrackRenderer.Ac3EventListener { +/** + * MPEG-2 TS stream player implementation using ExoPlayer. + */ +public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRenderer.EventListener, + Ac3PassthroughTrackRenderer.EventListener, Ac3TrackRenderer.Ac3EventListener { private int mCaptionServiceNumber = Cea708Data.EMPTY_SERVICE_NUMBER; /** @@ -305,10 +304,8 @@ public class MpegTsPlayer SoftPreconditions.checkState(supportSmoothTrickPlay(playbackParams.getSpeed())); mPlayer.setPlayWhenReady(true); mTrickplayRunning = true; - if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) { - mPlayer.sendMessage( - mAudioRenderer, - Ac3DefaultTrackRenderer.MSG_SET_PLAYBACK_SPEED, + if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) { + mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_PLAYBACK_SPEED, playbackParams.getSpeed()); } else { mPlayer.sendMessage(mAudioRenderer, @@ -320,9 +317,10 @@ public class MpegTsPlayer private void stopSmoothTrickplay(boolean calledBySeek) { if (mTrickplayRunning) { mTrickplayRunning = false; - if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) { - mPlayer.sendMessage( - mAudioRenderer, Ac3DefaultTrackRenderer.MSG_SET_PLAYBACK_SPEED, 1.0f); + if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) { + mPlayer.sendMessage(mAudioRenderer, + Ac3PassthroughTrackRenderer.MSG_SET_PLAYBACK_SPEED, + 1.0f); } else { mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_PLAYBACK_PARAMS, @@ -425,8 +423,8 @@ public class MpegTsPlayer */ public void setVolume(float volume) { mVolume = volume; - if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) { - mPlayer.sendMessage(mAudioRenderer, Ac3DefaultTrackRenderer.MSG_SET_VOLUME, volume); + if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) { + mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_VOLUME, volume); } else { mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, volume); @@ -439,9 +437,9 @@ public class MpegTsPlayer * @param enable enables the audio when {@code true}, disables otherwise. */ public void setAudioTrack(boolean enable) { - if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) { - mPlayer.sendMessage( - mAudioRenderer, Ac3DefaultTrackRenderer.MSG_SET_AUDIO_TRACK, enable ? 1 : 0); + if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) { + mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_AUDIO_TRACK, + enable ? 1 : 0); } else { mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, enable ? mVolume : 0.0f); @@ -497,28 +495,6 @@ public class MpegTsPlayer } /** - * Returns the index of the currently selected track for the specified renderer. - * - * @param rendererIndex The index of the renderer. - * @return The selected track. A negative value or a value greater than or equal to the renderer's - * track count indicates that the renderer is disabled. - */ - public int getSelectedTrack(int rendererIndex) { - return mPlayer.getSelectedTrack(rendererIndex); - } - - /** - * Returns the format of a track. - * - * @param rendererIndex The index of the renderer. - * @param trackIndex The index of the track. - * @return The format of the track. - */ - public MediaFormat getTrackFormat(int rendererIndex, int trackIndex) { - return mPlayer.getTrackFormat(rendererIndex, trackIndex); - } - - /** * Gets the main handler of the player. */ /* package */ Handler getMainHandler() { @@ -674,4 +650,4 @@ public class MpegTsPlayer } } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java index a1a97d3d..0e46c9cf 100644 --- a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java +++ b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java @@ -21,10 +21,9 @@ import android.content.Context; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.upstream.DataSource; -import com.android.tv.Features; import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilder; import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilderCallback; -import com.android.tv.tuner.exoplayer.ac3.Ac3DefaultTrackRenderer; +import com.android.tv.tuner.exoplayer.ac3.Ac3PassthroughTrackRenderer; import com.android.tv.tuner.exoplayer.buffer.BufferManager; import com.android.tv.tuner.tvinput.PlaybackBufferListener; @@ -53,12 +52,10 @@ public class MpegTsRendererBuilder implements RendererBuilder { SampleSource sampleSource = new MpegTsSampleSource(extractor); MpegTsVideoTrackRenderer videoRenderer = new MpegTsVideoTrackRenderer(mContext, sampleSource, mpegTsPlayer.getMainHandler(), mpegTsPlayer); - // TODO: Only using Ac3DefaultTrackRenderer for A/V sync issue. We will use - // {@link Ac3MediaCodecTrackRenderer} when we use ExoPlayer's extractor. - TrackRenderer audioRenderer = - new Ac3DefaultTrackRenderer( - sampleSource, mpegTsPlayer.getMainHandler(), mpegTsPlayer, - !Features.AC3_SOFTWARE_DECODE.isEnabled(mContext)); + // TODO: Only using Ac3PassthroughTrackRenderer for A/V sync issue. We will use + // {@link Ac3TrackRenderer} when we use ExoPlayer's extractor. + TrackRenderer audioRenderer = new Ac3PassthroughTrackRenderer(sampleSource, + mpegTsPlayer.getMainHandler(), mpegTsPlayer); Cea708TextTrackRenderer textRenderer = new Cea708TextTrackRenderer(sampleSource); TrackRenderer[] renderers = new TrackRenderer[MpegTsPlayer.RENDERER_COUNT]; diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java index d442fde8..9dae2e34 100644 --- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java +++ b/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java @@ -23,15 +23,16 @@ import android.util.Log; import com.google.android.exoplayer.CodecCounters; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.MediaClock; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; +import com.google.android.exoplayer.MediaFormatUtil; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; -import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.android.tv.tuner.tvinput.TunerDebug; import java.io.IOException; @@ -39,9 +40,9 @@ import java.nio.ByteBuffer; import java.util.ArrayList; /** - * Decodes and renders AC3 audio. Supports passthrough playback and ffmpeg based software decoding. + * Decodes and renders AC3 audio. */ -public class Ac3DefaultTrackRenderer extends TrackRenderer implements MediaClock { +public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaClock { public static final int MSG_SET_VOLUME = 10000; public static final int MSG_SET_AUDIO_TRACK = MSG_SET_VOLUME + 1; public static final int MSG_SET_PLAYBACK_SPEED = MSG_SET_VOLUME + 2; @@ -50,14 +51,7 @@ public class Ac3DefaultTrackRenderer extends TrackRenderer implements MediaClock // One AC3 sample has 1536 frames, and its duration is 32ms. public static final long AC3_SAMPLE_DURATION_US = 32000; - // This is around 150ms, 150ms is big enough not to under-run AudioTrack, - // and 150ms is also small enough to fill the buffer rapidly. - static int BUFFERED_SAMPLES_IN_AUDIOTRACK = 5; - public static final long INITIAL_AUDIO_BUFFERING_TIME_US = - BUFFERED_SAMPLES_IN_AUDIOTRACK * AC3_SAMPLE_DURATION_US; - - - private static final String TAG = "Ac3DefaultTrackRenderer"; + private static final String TAG = "Ac3PassthroughTrackRenderer"; private static final boolean DEBUG = false; /** @@ -99,8 +93,6 @@ public class Ac3DefaultTrackRenderer extends TrackRenderer implements MediaClock private final AudioClock mAudioClock; private MediaFormat mFormat; - private boolean mFormatConfigured; - private int mSampleSize; private final ByteBuffer mOutputBuffer; private boolean mOutputReady; private int mTrackIndex; @@ -114,15 +106,10 @@ public class Ac3DefaultTrackRenderer extends TrackRenderer implements MediaClock private long mInterpolatedTimeUs; private long mPreviousPositionUs; private boolean mIsStopped; - private boolean mEnabled = true; - private boolean mIsMuted; private ArrayList<Integer> mTracksIndex; - public Ac3DefaultTrackRenderer( - SampleSource source, - Handler eventHandler, - EventListener listener, - boolean usePassthrough) { + public Ac3PassthroughTrackRenderer(SampleSource source, Handler eventHandler, + EventListener listener) { mSource = source.register(); mEventHandler = eventHandler; mEventListener = listener; @@ -338,38 +325,14 @@ public class Ac3DefaultTrackRenderer extends TrackRenderer implements MediaClock } } - private MediaFormat convertMediaFormatToRaw(MediaFormat format) { - return MediaFormat.createAudioFormat( - format.trackId, - MimeTypes.AUDIO_RAW, - format.bitrate, - format.maxInputSize, - format.durationUs, - format.channelCount, - format.sampleRate, - format.initializationData, - format.language); - } - private void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException { - mFormat = formatHolder.format; - mFormatConfigured = true; + mFormat = formatHolder.format; if (DEBUG) { Log.d(TAG, "AudioTrack was configured to FORMAT: " + mFormat.toString()); } clearDecodeState(); - AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16(), 0); - } - - private void onSampleSizeChanged(int sampleSize) { - if (DEBUG) { - Log.d(TAG, "Sample size was changed to : " + sampleSize); - } - clearDecodeState(); - int audioBufferSize = sampleSize * BUFFERED_SAMPLES_IN_AUDIOTRACK; - mSampleSize = sampleSize; - AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16(), audioBufferSize); + AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16()); } private boolean feedInputBuffer() throws IOException, ExoPlaybackException { @@ -396,11 +359,8 @@ public class Ac3DefaultTrackRenderer extends TrackRenderer implements MediaClock return false; } default: { - if (mSampleHolder.size != mSampleSize && mFormatConfigured) { - onSampleSizeChanged(mSampleHolder.size); - } mSampleHolder.data.flip(); - decodeDone(mSampleHolder.data, mSampleHolder.timeUs); + decodeDone(mSampleHolder.data, mSampleHolder.timeUs); return true; } } @@ -551,29 +511,24 @@ public class Ac3DefaultTrackRenderer extends TrackRenderer implements MediaClock public void handleMessage(int messageType, Object message) throws ExoPlaybackException { switch (messageType) { case MSG_SET_VOLUME: - float volume = (Float) message; - // Workaround: we cannot mute the audio track by setting the volume to 0, we need to - // disable the AUDIO_TRACK for this intent. However, enabling/disabling audio track - // whenever volume is being set might cause side effects, therefore we only handle - // "explicit mute operations", i.e., only after certain non-zero volume has been - // set, the subsequent volume setting operations will be consider as mute/un-mute - // operations and thus enable/disable the audio track. - if (mIsMuted && volume > 0) { - mIsMuted = false; - if (mEnabled) { - setStatus(true); - } - } else if (!mIsMuted && volume == 0) { - mIsMuted = true; - if (mEnabled) { - setStatus(false); - } - } - AUDIO_TRACK.setVolume(volume); + AUDIO_TRACK.setVolume((Float) message); break; case MSG_SET_AUDIO_TRACK: - mEnabled = (Integer) message == 1; - setStatus(mEnabled); + boolean enabled = (Integer) message == 1; + if (enabled == AUDIO_TRACK.isEnabled()) { + return; + } + if (!enabled) { + // mAudioClock can be different from getPositionUs. In order to sync them, + // we set mAudioClock. + mAudioClock.setPositionUs(getPositionUs()); + } + AUDIO_TRACK.setStatus(enabled); + if (enabled) { + // When AUDIO_TRACK is enabled, we need to clear AUDIO_TRACK and seek to + // the current position. If not, AUDIO_TRACK has the obsolete data. + seekTo(mAudioClock.getPositionUs()); + } break; case MSG_SET_PLAYBACK_SPEED: mAudioClock.setPlaybackSpeed((Float) message); @@ -582,21 +537,4 @@ public class Ac3DefaultTrackRenderer extends TrackRenderer implements MediaClock super.handleMessage(messageType, message); } } - - private void setStatus(boolean enabled) { - if (enabled == AUDIO_TRACK.isEnabled()) { - return; - } - if (!enabled) { - // mAudioClock can be different from getPositionUs. In order to sync them, - // we set mAudioClock. - mAudioClock.setPositionUs(getPositionUs()); - } - AUDIO_TRACK.setStatus(enabled); - if (enabled) { - // When AUDIO_TRACK is enabled, we need to clear AUDIO_TRACK and seek to - // the current position. If not, AUDIO_TRACK has the obsolete data. - seekTo(mAudioClock.getPositionUs()); - } - } } diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java index 604959d1..2bf86b5a 100644 --- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java +++ b/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java @@ -25,14 +25,14 @@ import com.google.android.exoplayer.SampleSource; /** * MPEG-2 TS audio track renderer. - * - * <p>Since the audio output from {@link android.media.MediaExtractor} contains extra samples at the - * beginning, using original {@link MediaCodecAudioTrackRenderer} as audio renderer causes - * asynchronous Audio/Video outputs. This class calculates the offset of audio data and adjust the - * presentation times to avoid the asynchronous Audio/Video problem. + * <p>Since the audio output from {@link android.media.MediaExtractor} contains extra samples at + * the beginning, using original {@link MediaCodecAudioTrackRenderer} as audio renderer causes + * asynchronous Audio/Video outputs. + * This class calculates the offset of audio data and adjust the presentation times to avoid the + * asynchronous Audio/Video problem. */ -public class Ac3MediaCodecTrackRenderer extends MediaCodecAudioTrackRenderer { - private final String TAG = "Ac3MediaCodecTrackRenderer"; +public class Ac3TrackRenderer extends MediaCodecAudioTrackRenderer { + private final String TAG = "Ac3TrackRenderer"; private final boolean DEBUG = false; private final Ac3EventListener mListener; @@ -47,11 +47,8 @@ public class Ac3MediaCodecTrackRenderer extends MediaCodecAudioTrackRenderer { void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e); } - public Ac3MediaCodecTrackRenderer( - SampleSource source, - MediaCodecSelector mediaCodecSelector, - Handler eventHandler, - EventListener eventListener) { + public Ac3TrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector, + Handler eventHandler, EventListener eventListener) { super(source, mediaCodecSelector, eventHandler, eventListener); mListener = (Ac3EventListener) eventListener; } diff --git a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java index 6f152490..bfdf08ac 100644 --- a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java +++ b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java @@ -98,8 +98,8 @@ public class AudioTrackMonitor { long now = SystemClock.elapsedRealtime(); if (mExpireMs != 0 && now >= mExpireMs) { if (DEBUG) { - long sampleDuration = - (mTotalCount - 1) * Ac3DefaultTrackRenderer.AC3_SAMPLE_DURATION_US / 1000; + long sampleDuration = (mTotalCount - 1) * + Ac3PassthroughTrackRenderer.AC3_SAMPLE_DURATION_US / 1000; long totalDuration = now - mStartMs; StringBuilder ptsBuilder = new StringBuilder(); ptsBuilder.append("PTS received ").append(mSampleCount).append(", ") diff --git a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java index 393e12c3..bc3c5d00 100644 --- a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java +++ b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java @@ -18,7 +18,6 @@ package com.android.tv.tuner.exoplayer.ac3; import android.media.MediaFormat; -import com.google.android.exoplayer.C; import com.google.android.exoplayer.audio.AudioTrack; import java.nio.ByteBuffer; @@ -29,10 +28,6 @@ import java.nio.ByteBuffer; * This wrapper class will do nothing in disabled status for those operations. */ public class AudioTrackWrapper { - private static final int PCM16_FRAME_BYTES = 2; - private static final int AC3_FRAMES_IN_ONE_SAMPLE = 1536; - private static final int BUFFERED_SAMPLES_IN_AUDIOTRACK = - Ac3DefaultTrackRenderer.BUFFERED_SAMPLES_IN_AUDIOTRACK; private final AudioTrack mAudioTrack = new AudioTrack(); private int mAudioSessionID; private boolean mIsEnabled; @@ -111,7 +106,7 @@ public class AudioTrackWrapper { mAudioTrack.setVolume(volume); } - public void reconfigure(MediaFormat format, int audioBufferSize) { + public void reconfigure(MediaFormat format) { if (!mIsEnabled || format == null) { return; } @@ -122,9 +117,9 @@ public class AudioTrackWrapper { try { pcmEncoding = format.getInteger(MediaFormat.KEY_PCM_ENCODING); } catch (Exception e) { - pcmEncoding = C.ENCODING_PCM_16BIT; + pcmEncoding = com.google.android.exoplayer.MediaFormat.NO_VALUE; } - // TODO: Handle non-AC3. + // TODO: Handle non-AC3 or non-passthrough audio. if (MediaFormat.MIMETYPE_AUDIO_AC3.equalsIgnoreCase(mimeType) && channelCount != 2) { // Workarounds b/25955476. // Since all devices and platforms does not support passthrough for non-stereo AC3, @@ -132,14 +127,7 @@ public class AudioTrackWrapper { // In other words, the channel count should be always 2. channelCount = 2; } - if (MediaFormat.MIMETYPE_AUDIO_RAW.equalsIgnoreCase(mimeType)) { - audioBufferSize = - channelCount - * PCM16_FRAME_BYTES - * AC3_FRAMES_IN_ONE_SAMPLE - * BUFFERED_SAMPLES_IN_AUDIOTRACK; - } - mAudioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, audioBufferSize); + mAudioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding); } public void handleDiscontinuity() { diff --git a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java index 112e9dc4..eb596e93 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java @@ -25,14 +25,13 @@ import android.util.Log; import android.util.Pair; import com.google.android.exoplayer.SampleHolder; -import com.android.tv.common.SoftPreconditions; import com.android.tv.tuner.exoplayer.SampleExtractor; import com.android.tv.util.Utils; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -60,8 +59,7 @@ public class BufferManager { private final SampleChunk.SampleChunkCreator mSampleChunkCreator; // Maps from track name to a map which maps from starting position to {@link SampleChunk}. - private final Map<String, SortedMap<Long, Pair<SampleChunk, Integer>>> mChunkMap = - new ArrayMap<>(); + private final Map<String, SortedMap<Long, SampleChunk>> mChunkMap = new ArrayMap<>(); private final Map<String, Long> mStartPositionMap = new ArrayMap<>(); private final Map<String, ChunkEvictedListener> mEvictListeners = new ArrayMap<>(); private final StorageManager mStorageManager; @@ -79,11 +77,13 @@ public class BufferManager { } }; + private volatile boolean mClosed = false; private int mMinSampleSizeForSpeedCheck = MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK; private long mTotalWriteSize; private long mTotalWriteTimeNs; private float mWriteBandwidth = 0.0f; private volatile int mSpeedCheckCount; + private boolean mDisabled = false; public interface ChunkEvictedListener { void onChunkEvicted(String id, long createdTimeMs); @@ -174,66 +174,6 @@ public class BufferManager { } /** - * A Track format which will be loaded and saved from the permanent storage for recordings. - */ - public static class TrackFormat { - - /** - * The track id for the specified track. The track id will be used as a track identifier - * for recordings. - */ - public final String trackId; - - /** - * The {@link MediaFormat} for the specified track. - */ - public final MediaFormat format; - - /** - * Creates TrackFormat. - * @param trackId - * @param format - */ - public TrackFormat(String trackId, MediaFormat format) { - this.trackId = trackId; - this.format = format; - } - } - - /** - * A Holder for a sample position which will be loaded from the index file for recordings. - */ - public static class PositionHolder { - - /** - * The current sample position in microseconds. - * The position is identical to the PTS(presentation time stamp) of the sample. - */ - public final long positionUs; - - /** - * Base sample position for the current {@link SampleChunk}. - */ - public final long basePositionUs; - - /** - * The file offset for the current sample in the current {@link SampleChunk}. - */ - public final int offset; - - /** - * Creates a holder for a specific position in the recording. - * @param positionUs - * @param offset - */ - public PositionHolder(long positionUs, long basePositionUs, int offset) { - this.positionUs = positionUs; - this.basePositionUs = basePositionUs; - this.offset = offset; - } - } - - /** * Storage configuration and policy manager for {@link BufferManager} */ public interface StorageManager { @@ -246,6 +186,11 @@ public class BufferManager { File getBufferDir(); /** + * Cleans up storage. + */ + void clearStorage(); + + /** * Informs whether the storage is used for persistent use. (eg. dvr recording/play) * * @return {@code true} if stored files are persistent @@ -275,27 +220,29 @@ public class BufferManager { * Reads track name & {@link MediaFormat} from storage. * * @param isAudio {@code true} if it is for audio track - * @return {@link List} of TrackFormat + * @return {@link Pair} of track name & {@link MediaFormat} + * @throws IOException */ - List<TrackFormat> readTrackInfoFiles(boolean isAudio); + Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException; /** - * Reads key sample positions for each written sample from storage. + * Reads sample indexes for each written sample from storage. * * @param trackId track name * @return indexes of the specified track * @throws IOException */ - ArrayList<PositionHolder> readIndexFile(String trackId) throws IOException; + ArrayList<Long> readIndexFile(String trackId) throws IOException; /** * Writes track information to storage. * - * @param formatList {@list List} of TrackFormat + * @param trackId track name + * @param format {@link android.media.MediaFormat} of the track * @param isAudio {@code true} if it is for audio track * @throws IOException */ - void writeTrackInfoFiles(List<TrackFormat> formatList, boolean isAudio) + void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio) throws IOException; /** @@ -305,7 +252,7 @@ public class BufferManager { * @param index {@link SampleChunk} container * @throws IOException */ - void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index) + void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index) throws IOException; } @@ -360,6 +307,7 @@ public class BufferManager { SampleChunk.SampleChunkCreator sampleChunkCreator) { mStorageManager = storageManager; mSampleChunkCreator = sampleChunkCreator; + clearBuffer(true); } public void registerChunkEvictedListener(String id, ChunkEvictedListener listener) { @@ -370,44 +318,44 @@ public class BufferManager { mEvictListeners.remove(id); } + private void clearBuffer(boolean deleteFiles) { + mChunkMap.clear(); + if (deleteFiles) { + mStorageManager.clearStorage(); + } + mBufferSize = 0; + } + private static String getFileName(String id, long positionUs) { return String.format(Locale.ENGLISH, "%s_%016x.chunk", id, positionUs); } /** - * Creates a new {@link SampleChunk} for caching samples if it is needed. + * Creates a new {@link SampleChunk} for caching samples. * * @param id the name of the track - * @param positionUs current position to write a sample in micro seconds. + * @param positionUs starting position of the {@link SampleChunk} in micro seconds. * @param samplePool {@link SamplePool} for the fast creation of samples. - * @param currentChunk the current {@link SampleChunk} to write, {@code null} when to create - * a new {@link SampleChunk}. - * @param currentOffset the current offset to write. * @return returns the created {@link SampleChunk}. * @throws IOException */ - public SampleChunk createNewWriteFileIfNeeded(String id, long positionUs, SamplePool samplePool, - SampleChunk currentChunk, int currentOffset) throws IOException { + public SampleChunk createNewWriteFile(String id, long positionUs, + SamplePool samplePool) throws IOException { if (!maybeEvictChunk()) { throw new IOException("Not enough storage space"); } - SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id); + SortedMap<Long, SampleChunk> map = mChunkMap.get(id); if (map == null) { map = new TreeMap<>(); mChunkMap.put(id, map); mStartPositionMap.put(id, positionUs); mPendingDelete.init(id); } - if (currentChunk == null) { - File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs)); - SampleChunk sampleChunk = mSampleChunkCreator - .createSampleChunk(samplePool, file, positionUs, mChunkCallback); - map.put(positionUs, new Pair(sampleChunk, 0)); - return sampleChunk; - } else { - map.put(positionUs, new Pair(currentChunk, currentOffset)); - return null; - } + File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs)); + SampleChunk sampleChunk = mSampleChunkCreator.createSampleChunk(samplePool, file, + positionUs, mChunkCallback); + map.put(positionUs, sampleChunk); + return sampleChunk; } /** @@ -418,10 +366,10 @@ public class BufferManager { * @throws IOException */ public void loadTrackFromStorage(String trackId, SamplePool samplePool) throws IOException { - ArrayList<PositionHolder> keyPositions = mStorageManager.readIndexFile(trackId); - long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0).positionUs : 0; + ArrayList<Long> keyPositions = mStorageManager.readIndexFile(trackId); + long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0) : 0; - SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(trackId); + SortedMap<Long, SampleChunk> map = mChunkMap.get(trackId); if (map == null) { map = new TreeMap<>(); mChunkMap.put(trackId, map); @@ -429,15 +377,11 @@ public class BufferManager { mPendingDelete.init(trackId); } SampleChunk chunk = null; - long basePositionUs = -1; - for (PositionHolder position: keyPositions) { - if (position.basePositionUs != basePositionUs) { - chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool, - mStorageManager.getBufferDir(), getFileName(trackId, position.positionUs), - position.positionUs, mChunkCallback, chunk); - basePositionUs = position.basePositionUs; - } - map.put(position.positionUs, new Pair(chunk, position.offset)); + for (long positionUs: keyPositions) { + chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool, + mStorageManager.getBufferDir(), getFileName(trackId, positionUs), positionUs, + mChunkCallback, chunk); + map.put(positionUs, chunk); } } @@ -448,19 +392,19 @@ public class BufferManager { * @param positionUs the position. * @return returns the found {@link SampleChunk}. */ - public Pair<SampleChunk, Integer> getReadFile(String id, long positionUs) { - SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id); + public SampleChunk getReadFile(String id, long positionUs) { + SortedMap<Long, SampleChunk> map = mChunkMap.get(id); if (map == null) { return null; } - Pair<SampleChunk, Integer> ret; - SortedMap<Long, Pair<SampleChunk, Integer>> headMap = map.headMap(positionUs + 1); + SampleChunk sampleChunk; + SortedMap<Long, SampleChunk> headMap = map.headMap(positionUs + 1); if (!headMap.isEmpty()) { - ret = headMap.get(headMap.lastKey()); + sampleChunk = headMap.get(headMap.lastKey()); } else { - ret = map.get(map.firstKey()); + sampleChunk = map.get(map.firstKey()); } - return ret; + return sampleChunk; } /** @@ -495,16 +439,15 @@ public class BufferManager { // Since chunks are persistent, we cannot evict chunks. return false; } - SortedMap<Long, Pair<SampleChunk, Integer>> earliestChunkMap = null; + SortedMap<Long, SampleChunk> earliestChunkMap = null; SampleChunk earliestChunk = null; String earliestChunkId = null; - for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry : - mChunkMap.entrySet()) { - SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue(); + for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) { + SortedMap<Long, SampleChunk> map = entry.getValue(); if (map.isEmpty()) { continue; } - SampleChunk chunk = map.get(map.firstKey()).first; + SampleChunk chunk = map.get(map.firstKey()); if (earliestChunk == null || chunk.getCreatedTimeMs() < earliestChunk.getCreatedTimeMs()) { earliestChunkMap = map; @@ -530,9 +473,8 @@ public class BufferManager { } pendingDelete = mPendingDelete.getSize(); } - for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry : - mChunkMap.entrySet()) { - SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue(); + for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) { + SortedMap<Long, SampleChunk> map = entry.getValue(); if (map.isEmpty()) { continue; } @@ -547,74 +489,70 @@ public class BufferManager { * @return returns all track information which is found by {@link BufferManager.StorageManager}. * @throws IOException */ - public List<TrackFormat> readTrackInfoFiles() throws IOException { - List<TrackFormat> trackFormatList = new ArrayList<>(); - trackFormatList.addAll(mStorageManager.readTrackInfoFiles(false)); - trackFormatList.addAll(mStorageManager.readTrackInfoFiles(true)); - if (trackFormatList.isEmpty()) { - throw new IOException("No track information to load"); + public ArrayList<Pair<String, MediaFormat>> readTrackInfoFiles() throws IOException { + ArrayList<Pair<String, MediaFormat>> trackInfos = new ArrayList<>(); + try { + trackInfos.add(mStorageManager.readTrackInfoFile(false)); + } catch (FileNotFoundException e) { + // There can be a single track only recording. (eg. audio-only, video-only) + // So the exception should not stop the read. } - return trackFormatList; + try { + trackInfos.add(mStorageManager.readTrackInfoFile(true)); + } catch (FileNotFoundException e) { + // See above catch block. + } + return trackInfos; } /** * Writes track information and index information for all tracks. * - * @param audios list of audio track information - * @param videos list of audio track information + * @param audio audio information. + * @param video video information. * @throws IOException */ - public void writeMetaFiles(List<TrackFormat> audios, List<TrackFormat> videos) + public void writeMetaFiles(Pair<String, MediaFormat> audio, Pair<String, MediaFormat> video) throws IOException { - if (audios.isEmpty() && videos.isEmpty()) { - throw new IOException("No track information to save"); - } - if (!audios.isEmpty()) { - mStorageManager.writeTrackInfoFiles(audios, true); - for (TrackFormat trackFormat : audios) { - SortedMap<Long, Pair<SampleChunk, Integer>> map = - mChunkMap.get(trackFormat.trackId); - if (map == null) { - throw new IOException("Audio track index missing"); - } - mStorageManager.writeIndexFile(trackFormat.trackId, map); + if (audio != null) { + mStorageManager.writeTrackInfoFile(audio.first, audio.second, true); + SortedMap<Long, SampleChunk> map = mChunkMap.get(audio.first); + if (map == null) { + throw new IOException("Audio track index missing"); } + mStorageManager.writeIndexFile(audio.first, map); } - if (!videos.isEmpty()) { - mStorageManager.writeTrackInfoFiles(videos, false); - for (TrackFormat trackFormat : videos) { - SortedMap<Long, Pair<SampleChunk, Integer>> map = - mChunkMap.get(trackFormat.trackId); - if (map == null) { - throw new IOException("Video track index missing"); - } - mStorageManager.writeIndexFile(trackFormat.trackId, map); + if (video != null) { + mStorageManager.writeTrackInfoFile(video.first, video.second, false); + SortedMap<Long, SampleChunk> map = mChunkMap.get(video.first); + if (map == null) { + throw new IOException("Video track index missing"); } + mStorageManager.writeIndexFile(video.first, map); } } /** + * Marks it is closed and it is not used anymore. + */ + public void close() { + // Clean-up may happen after this is called. + mClosed = true; + } + + /** * Releases all the resources. */ public void release() { - try { - mPendingDelete.release(); - for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry : - mChunkMap.entrySet()) { - SampleChunk toRelease = null; - for (Pair<SampleChunk, Integer> positions : entry.getValue().values()) { - if (toRelease != positions.first) { - toRelease = positions.first; - SampleChunk.IoState.release(toRelease, !mStorageManager.isPersistent()); - } - } + mPendingDelete.release(); + for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) { + for (SampleChunk chunk : entry.getValue().values()) { + SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent()); } - mChunkMap.clear(); - } catch (ConcurrentModificationException | NullPointerException e) { - // TODO: remove this after it it confirmed that race condition issues are resolved. - // b/32492258, b/32373376 - SoftPreconditions.checkState(false, "Exception on BufferManager#release: ", - e.toString()); + } + mChunkMap.clear(); + if (mClosed) { + clearBuffer(!mStorageManager.isPersistent()); } } @@ -673,6 +611,20 @@ public class BufferManager { } /** + * Marks {@link BufferManager} object disabled to prevent it from the future use. + */ + public void disable() { + mDisabled = true; + } + + /** + * Returns if {@link BufferManager} object is disabled. + */ + public boolean isDisabled() { + return mDisabled; + } + + /** * Returns if {@link BufferManager} has checked the write speed, * which is suitable for Trickplay. */ diff --git a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java index bea3defd..6a0502a7 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java @@ -17,12 +17,8 @@ package com.android.tv.tuner.exoplayer.buffer; import android.media.MediaFormat; -import android.util.Log; import android.util.Pair; -import com.android.tv.tuner.data.Track.AtscCaptionTrack; -import com.google.protobuf.nano.MessageNano; - import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; @@ -32,25 +28,18 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.List; -import java.util.Map; import java.util.SortedMap; /** * Manages DVR storage. */ public class DvrStorageManager implements BufferManager.StorageManager { - private static final String TAG = "DvrStorageManager"; // TODO: make serializable classes and use protobuf after internal data structure is finalized. private static final String KEY_PIXEL_WIDTH_HEIGHT_RATIO = "com.google.android.videos.pixelWidthHeightRatio"; - private static final String META_FILE_TYPE_AUDIO = "audio"; - private static final String META_FILE_TYPE_VIDEO = "video"; - private static final String META_FILE_TYPE_CAPTION = "caption"; private static final String META_FILE_SUFFIX = ".meta"; private static final String IDX_FILE_SUFFIX = ".idx"; - private static final String IDX_FILE_SUFFIX_V2 = IDX_FILE_SUFFIX + "2"; // Size of minimum reserved storage buffer which will be used to save meta files // and index files after actual recording finished. @@ -70,6 +59,18 @@ public class DvrStorageManager implements BufferManager.StorageManager { } @Override + public void clearStorage() { + if (mIsRecording) { + File[] files = mBufferDir.listFiles(); + if (files != null && files.length > 0) { + for (File file : files) { + file.delete(); + } + } + } + } + + @Override public File getBufferDir() { return mBufferDir; } @@ -131,17 +132,6 @@ public class DvrStorageManager implements BufferManager.StorageManager { } } - private void readFormatStringOptional(DataInputStream in, MediaFormat format, String key) { - try { - String str = readString(in); - if (str != null) { - format.setString(key, str); - } - } catch (IOException e) { - // Since we are reading optional field, ignore the exception. - } - } - private ByteBuffer readByteBuffer(DataInputStream in) throws IOException { int len = in.readInt(); if (len <= 0) { @@ -165,104 +155,39 @@ public class DvrStorageManager implements BufferManager.StorageManager { } @Override - public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) { - List<BufferManager.TrackFormat> trackFormatList = new ArrayList<>(); - int index = 0; - boolean trackNotFound = false; - do { - String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO) - + ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX)); - File file = new File(getBufferDir(), fileName); - try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { - String name = readString(in); - MediaFormat format = new MediaFormat(); - readFormatString(in, format, MediaFormat.KEY_MIME); - readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE); - readFormatInt(in, format, MediaFormat.KEY_WIDTH); - readFormatInt(in, format, MediaFormat.KEY_HEIGHT); - readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT); - readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE); - readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); - for (int i = 0; i < 3; ++i) { - readFormatByteBuffer(in, format, "csd-" + i); - } - readFormatLong(in, format, MediaFormat.KEY_DURATION); - - // This is optional since language field is added later. - readFormatStringOptional(in, format, MediaFormat.KEY_LANGUAGE); - trackFormatList.add(new BufferManager.TrackFormat(name, format)); - } catch (IOException e) { - trackNotFound = true; + public Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException { + File file = new File(getBufferDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX); + try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { + String name = readString(in); + MediaFormat format = new MediaFormat(); + readFormatString(in, format, MediaFormat.KEY_MIME); + readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE); + readFormatInt(in, format, MediaFormat.KEY_WIDTH); + readFormatInt(in, format, MediaFormat.KEY_HEIGHT); + readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT); + readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE); + readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); + for (int i = 0; i < 3; ++i) { + readFormatByteBuffer(in, format, "csd-" + i); } - index++; - } while(!trackNotFound); - return trackFormatList; - } - - /** - * Reads caption information from files. - * - * @return a list of {@link AtscCaptionTrack} objects which store caption information. - */ - public List<AtscCaptionTrack> readCaptionInfoFiles() { - List<AtscCaptionTrack> tracks = new ArrayList<>(); - int index = 0; - boolean trackNotFound = false; - do { - String fileName = META_FILE_TYPE_CAPTION + - ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX)); - File file = new File(getBufferDir(), fileName); - try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { - byte[] data = new byte[(int) file.length()]; - in.read(data); - tracks.add(AtscCaptionTrack.parseFrom(data)); - } catch (IOException e) { - trackNotFound = true; - } - index++; - } while(!trackNotFound); - return tracks; - } - - private ArrayList<BufferManager.PositionHolder> readOldIndexFile(File indexFile) - throws IOException { - ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>(); - try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) { - long count = in.readLong(); - for (long i = 0; i < count; ++i) { - long positionUs = in.readLong(); - indices.add(new BufferManager.PositionHolder(positionUs, positionUs, 0)); - } - return indices; + readFormatLong(in, format, MediaFormat.KEY_DURATION); + return new Pair<>(name, format); } } - private ArrayList<BufferManager.PositionHolder> readNewIndexFile(File indexFile) - throws IOException { - ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>(); - try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) { + @Override + public ArrayList<Long> readIndexFile(String trackId) throws IOException { + ArrayList<Long> indices = new ArrayList<>(); + File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX); + try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { long count = in.readLong(); for (long i = 0; i < count; ++i) { - long positionUs = in.readLong(); - long basePositionUs = in.readLong(); - int offset = in.readInt(); - indices.add(new BufferManager.PositionHolder(positionUs, basePositionUs, offset)); + indices.add(in.readLong()); } return indices; } } - @Override - public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId) - throws IOException { - File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX_V2); - if (file.exists()) { - return readNewIndexFile(file); - } else { - return readOldIndexFile(new File(getBufferDir(),trackId + IDX_FILE_SUFFIX)); - } - } - private void writeFormatInt(DataOutputStream out, MediaFormat format, String key) throws IOException { if (format.containsKey(key)) { @@ -329,63 +254,33 @@ public class DvrStorageManager implements BufferManager.StorageManager { } @Override - public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio) + public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio) throws IOException { - for (int i = 0; i < formatList.size() ; ++i) { - BufferManager.TrackFormat trackFormat = formatList.get(i); - String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO) - + ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX)); - File file = new File(getBufferDir(), fileName); - try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) { - writeString(out, trackFormat.trackId); - writeFormatString(out, trackFormat.format, MediaFormat.KEY_MIME); - writeFormatInt(out, trackFormat.format, MediaFormat.KEY_MAX_INPUT_SIZE); - writeFormatInt(out, trackFormat.format, MediaFormat.KEY_WIDTH); - writeFormatInt(out, trackFormat.format, MediaFormat.KEY_HEIGHT); - writeFormatInt(out, trackFormat.format, MediaFormat.KEY_CHANNEL_COUNT); - writeFormatInt(out, trackFormat.format, MediaFormat.KEY_SAMPLE_RATE); - writeFormatFloat(out, trackFormat.format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); - for (int j = 0; j < 3; ++j) { - writeFormatByteBuffer(out, trackFormat.format, "csd-" + j); - } - writeFormatLong(out, trackFormat.format, MediaFormat.KEY_DURATION); - writeFormatString(out, trackFormat.format, MediaFormat.KEY_LANGUAGE); - } - } - } - - /** - * Writes caption information to files. - * - * @param tracks a list of {@link AtscCaptionTrack} objects which store caption information. - */ - public void writeCaptionInfoFiles(List<AtscCaptionTrack> tracks) { - if (tracks == null || tracks.isEmpty()) { - return; - } - for (int i = 0; i < tracks.size(); i++) { - AtscCaptionTrack track = tracks.get(i); - String fileName = META_FILE_TYPE_CAPTION + - ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX)); - File file = new File(getBufferDir(), fileName); - try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) { - out.write(MessageNano.toByteArray(track)); - } catch (Exception e) { - Log.e(TAG, "Fail to write caption info to files", e); + File file = new File(getBufferDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX); + try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) { + writeString(out, trackId); + writeFormatString(out, format, MediaFormat.KEY_MIME); + writeFormatInt(out, format, MediaFormat.KEY_MAX_INPUT_SIZE); + writeFormatInt(out, format, MediaFormat.KEY_WIDTH); + writeFormatInt(out, format, MediaFormat.KEY_HEIGHT); + writeFormatInt(out, format, MediaFormat.KEY_CHANNEL_COUNT); + writeFormatInt(out, format, MediaFormat.KEY_SAMPLE_RATE); + writeFormatFloat(out, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); + for (int i = 0; i < 3; ++i) { + writeFormatByteBuffer(out, format, "csd-" + i); } + writeFormatLong(out, format, MediaFormat.KEY_DURATION); } } @Override - public void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index) + public void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index) throws IOException { - File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX_V2); + File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX); try (DataOutputStream out = new DataOutputStream(new FileOutputStream(indexFile))) { out.writeLong(index.size()); - for (Map.Entry<Long, Pair<SampleChunk, Integer>> entry : index.entrySet()) { - out.writeLong(entry.getKey()); - out.writeLong(entry.getValue().first.getStartPositionUs()); - out.writeInt(entry.getValue().second); + for (Long key : index.keySet()) { + out.writeLong(key); } } } diff --git a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java index af0c3f0d..4869b49f 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java @@ -66,14 +66,9 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, public static final int BUFFER_REASON_RECORDING = 2; /** - * The minimum duration to support seek in Trickplay. + * The duration of a chunk of samples, {@link SampleChunk}. */ - static final long MIN_SEEK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500); - - /** - * The duration of a {@link SampleChunk} for recordings. - */ - static final long RECORDING_CHUNK_DURATION_US = MIN_SEEK_DURATION_US * 1200; // 10 minutes + static final long CHUNK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500); private static final long BUFFER_WRITE_TIMEOUT_MS = 10 * 1000; // 10 seconds private static final long BUFFER_NEEDED_US = 1000L * Math.max(MpegTsPlayer.MIN_BUFFER_MS, MpegTsPlayer.MIN_REBUFFER_MS); @@ -84,6 +79,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, private int mTrackCount; private boolean[] mTrackSelected; + private List<String> mIds; private List<SampleQueue> mReadSampleQueues; private final SamplePool mSamplePool = new SamplePool(); private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US; @@ -134,6 +130,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, if (mTrackCount <= 0) { throw new IOException("No tracks to initialize"); } + mIds = ids; mTrackSelected = new boolean[mTrackCount]; mReadSampleQueues = new ArrayList<>(); mSampleChunkIoHelper = new SampleChunkIoHelper(ids, mediaFormats, mBufferReason, @@ -142,9 +139,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, mReadSampleQueues.add(i, new SampleQueue(mSamplePool)); } mSampleChunkIoHelper.init(); - for (int i = 0; i < mTrackCount; ++i) { - mBufferManager.registerChunkEvictedListener(ids.get(i), RecordingSampleBuffer.this); - } } @Override @@ -152,6 +146,8 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, if (!mTrackSelected[index]) { mTrackSelected[index] = true; mReadSampleQueues.get(index).clear(); + mBufferManager.registerChunkEvictedListener(mIds.get(index), + RecordingSampleBuffer.this); mSampleChunkIoHelper.openRead(index, mCurrentPlaybackPositionUs); } } @@ -161,7 +157,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, if (mTrackSelected[index]) { mTrackSelected[index] = false; mReadSampleQueues.get(index).clear(); - mSampleChunkIoHelper.closeRead(index); + mBufferManager.unregisterChunkEvictedListener(mIds.get(index)); } } @@ -197,6 +193,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, } // Disables buffering samples afterwards, and notifies the disk speed is slow. Log.w(TAG, "Disk is too slow for trickplay"); + mBufferManager.disable(); mBufferListener.onDiskTooSlow(); } @@ -208,7 +205,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, private boolean maybeReadSample(SampleQueue queue, int index) { if (queue.getLastQueuedPositionUs() != null && queue.getLastQueuedPositionUs() > mCurrentPlaybackPositionUs + BUFFER_NEEDED_US - && queue.isDurationGreaterThan(MIN_SEEK_DURATION_US)) { + && queue.isDurationGreaterThan(CHUNK_DURATION_US)) { // The speed of queuing samples can be higher than the playback speed. // If the duration of the samples in the queue is not limited, // samples can be accumulated and there can be out-of-memory issues. @@ -303,7 +300,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, public void onChunkEvicted(String id, long createdTimeMs) { if (mBufferListener != null) { mBufferListener.onBufferStartTimeChanged( - createdTimeMs + TimeUnit.MICROSECONDS.toMillis(MIN_SEEK_DURATION_US)); + createdTimeMs + TimeUnit.MICROSECONDS.toMillis(CHUNK_DURATION_US)); } } } diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java index ab6d1a75..552caaef 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java @@ -151,23 +151,18 @@ public class SampleChunk { mCurrentOffset = 0; } - private void reset(SampleChunk chunk, long offset) { - mChunk = chunk; - mCurrentOffset = offset; - } - /** * Prepares for read I/O operation from a new SampleChunk. * * @param chunk the new SampleChunk to read from * @throws IOException */ - void openRead(SampleChunk chunk, long offset) throws IOException { + void openRead(SampleChunk chunk) throws IOException { if (mChunk != null) { mChunk.closeRead(); } chunk.openRead(); - reset(chunk, offset); + reset(chunk); } /** @@ -246,20 +241,6 @@ public class SampleChunk { } /** - * Returns the current SampleChunk for subsequent I/O operation. - */ - SampleChunk getChunk() { - return mChunk; - } - - /** - * Returns the current offset of the current SampleChunk for subsequent I/O operation. - */ - long getOffset() { - return mCurrentOffset; - } - - /** * Releases SampleChunk. the SampleChunk will not be used anymore. * * @param chunk to release diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java index ca97a91a..37ae4022 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java @@ -21,7 +21,6 @@ import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; -import android.util.ArraySet; import android.util.Log; import android.util.Pair; @@ -32,9 +31,7 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer.BufferReason; import java.io.IOException; -import java.util.LinkedList; import java.util.List; -import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; /** @@ -49,13 +46,11 @@ public class SampleChunkIoHelper implements Handler.Callback { private static final int MSG_OPEN_READ = 1; private static final int MSG_OPEN_WRITE = 2; - private static final int MSG_CLOSE_READ = 3; - private static final int MSG_CLOSE_WRITE = 4; - private static final int MSG_READ = 5; - private static final int MSG_WRITE = 6; - private static final int MSG_RELEASE = 7; + private static final int MSG_CLOSE_WRITE = 3; + private static final int MSG_READ = 4; + private static final int MSG_WRITE = 5; + private static final int MSG_RELEASE = 6; - private final long mSampleChunkDurationUs; private final int mTrackCount; private final List<String> mIds; private final List<MediaFormat> mMediaFormats; @@ -67,11 +62,9 @@ public class SampleChunkIoHelper implements Handler.Callback { private Handler mIoHandler; private final ConcurrentLinkedQueue<SampleHolder> mReadSampleBuffers[]; private final ConcurrentLinkedQueue<SampleHolder> mHandlerReadSampleBuffers[]; - private final long[] mWriteIndexEndPositionUs; - private final long[] mWriteChunkEndPositionUs; + private final long[] mWriteEndPositionUs; private final SampleChunk.IoState[] mReadIoStates; private final SampleChunk.IoState[] mWriteIoStates; - private final Set<Integer> mSelectedTracks = new ArraySet<>(); private long mBufferDurationUs = 0; private boolean mWriteEnded; private boolean mErrorNotified; @@ -136,20 +129,11 @@ public class SampleChunkIoHelper implements Handler.Callback { mReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount]; mHandlerReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount]; - mWriteIndexEndPositionUs = new long[mTrackCount]; - mWriteChunkEndPositionUs = new long[mTrackCount]; + mWriteEndPositionUs = new long[mTrackCount]; mReadIoStates = new SampleChunk.IoState[mTrackCount]; mWriteIoStates = new SampleChunk.IoState[mTrackCount]; - - // Small chunk duration for live playback will give more fine grained storage usage - // and eviction handling for trickplay. - mSampleChunkDurationUs = - bufferReason == RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK ? - RecordingSampleBuffer.MIN_SEEK_DURATION_US : - RecordingSampleBuffer.RECORDING_CHUNK_DURATION_US; for (int i = 0; i < mTrackCount; ++i) { - mWriteIndexEndPositionUs[i] = RecordingSampleBuffer.MIN_SEEK_DURATION_US; - mWriteChunkEndPositionUs[i] = mSampleChunkDurationUs; + mWriteEndPositionUs[i] = RecordingSampleBuffer.CHUNK_DURATION_US; mReadIoStates[i] = new SampleChunk.IoState(); mWriteIoStates[i] = new SampleChunk.IoState(); } @@ -220,15 +204,6 @@ public class SampleChunkIoHelper implements Handler.Callback { } /** - * Closes read from the specified track. - * - * @param index track index - */ - public void closeRead(int index) { - mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_CLOSE_READ, index)); - } - - /** * Notifies writes are finished. */ public void closeWrite() { @@ -254,19 +229,21 @@ public class SampleChunkIoHelper implements Handler.Callback { try { if (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING && mTrackCount > 0) { // Saves meta information for recording. - List<BufferManager.TrackFormat> audios = new LinkedList<>(); - List<BufferManager.TrackFormat> videos = new LinkedList<>(); + Pair<String, android.media.MediaFormat> audio = null, video = null; for (int i = 0; i < mTrackCount; ++i) { android.media.MediaFormat format = mMediaFormats.get(i).getFrameworkMediaFormatV16(); format.setLong(android.media.MediaFormat.KEY_DURATION, mBufferDurationUs); - if (MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) { - audios.add(new BufferManager.TrackFormat(mIds.get(i), format)); - } else if (MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) { - videos.add(new BufferManager.TrackFormat(mIds.get(i), format)); + if (audio == null && MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) { + audio = new Pair<>(mIds.get(i), format); + } else if (video == null && MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) { + video = new Pair<>(mIds.get(i), format); + } + if (audio != null && video != null) { + break; } } - mBufferManager.writeMetaFiles(audios, videos); + mBufferManager.writeMetaFiles(audio, video); } } finally { mBufferManager.release(); @@ -288,9 +265,6 @@ public class SampleChunkIoHelper implements Handler.Callback { case MSG_OPEN_WRITE: doOpenWrite((int) message.obj); return true; - case MSG_CLOSE_READ: - doCloseRead((int) message.obj); - return true; case MSG_CLOSE_WRITE: doCloseWrite(); return true; @@ -317,16 +291,14 @@ public class SampleChunkIoHelper implements Handler.Callback { private void doOpenRead(IoParams params) throws IOException { int index = params.index; mIoHandler.removeMessages(MSG_READ, index); - Pair<SampleChunk, Integer> readPosition = - mBufferManager.getReadFile(mIds.get(index), params.positionUs); - if (readPosition == null) { + SampleChunk chunk = mBufferManager.getReadFile(mIds.get(index), params.positionUs); + if (chunk == null) { String errorMessage = "Chunk ID:" + mIds.get(index) + " pos:" + params.positionUs + "is not found"; - SoftPreconditions.checkNotNull(readPosition, TAG, errorMessage); + SoftPreconditions.checkNotNull(chunk, TAG, errorMessage); throw new IOException(errorMessage); } - mSelectedTracks.add(index); - mReadIoStates[index].openRead(readPosition.first, (long) readPosition.second); + mReadIoStates[index].openRead(chunk); if (mHandlerReadSampleBuffers[index] != null) { SampleHolder sample; while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) { @@ -338,22 +310,10 @@ public class SampleChunkIoHelper implements Handler.Callback { } private void doOpenWrite(int index) throws IOException { - SampleChunk chunk = mBufferManager.createNewWriteFileIfNeeded(mIds.get(index), 0, - mSamplePool, null, 0); + SampleChunk chunk = mBufferManager.createNewWriteFile(mIds.get(index), 0, mSamplePool); mWriteIoStates[index].openWrite(chunk); } - private void doCloseRead(int index) { - mSelectedTracks.remove(index); - if (mHandlerReadSampleBuffers[index] != null) { - SampleHolder sample; - while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) { - mSamplePool.releaseSample(sample); - } - } - mIoHandler.removeMessages(MSG_READ, index); - } - private void doRead(int index) throws IOException { mIoHandler.removeMessages(MSG_READ, index); if (mHandlerReadSampleBuffers[index].size() >= MAX_READ_BUFFER_SAMPLES) { @@ -397,21 +357,13 @@ public class SampleChunkIoHelper implements Handler.Callback { if (sample.timeUs > mBufferDurationUs) { mBufferDurationUs = sample.timeUs; } - if (sample.timeUs >= mWriteIndexEndPositionUs[index]) { - SampleChunk currentChunk = sample.timeUs >= mWriteChunkEndPositionUs[index] ? - null : mWriteIoStates[params.index].getChunk(); - int currentOffset = (int) mWriteIoStates[params.index].getOffset(); - nextChunk = mBufferManager.createNewWriteFileIfNeeded( - mIds.get(index), mWriteIndexEndPositionUs[index], mSamplePool, - currentChunk, currentOffset); - mWriteIndexEndPositionUs[index] = - ((sample.timeUs / RecordingSampleBuffer.MIN_SEEK_DURATION_US) + 1) * - RecordingSampleBuffer.MIN_SEEK_DURATION_US; - if (nextChunk != null) { - mWriteChunkEndPositionUs[index] = - ((sample.timeUs / mSampleChunkDurationUs) + 1) - * mSampleChunkDurationUs; - } + + if (sample.timeUs >= mWriteEndPositionUs[index]) { + nextChunk = mBufferManager.createNewWriteFile(mIds.get(index), + mWriteEndPositionUs[index], mSamplePool); + mWriteEndPositionUs[index] = + ((sample.timeUs / RecordingSampleBuffer.CHUNK_DURATION_US) + 1) * + RecordingSampleBuffer.CHUNK_DURATION_US; } } mWriteIoStates[params.index].write(params.sample, nextChunk); @@ -439,22 +391,15 @@ public class SampleChunkIoHelper implements Handler.Callback { mIoHandler.removeCallbacksAndMessages(null); mFinished = true; conditionVariable.open(); - mSelectedTracks.clear(); } private void releaseEvictedChunks() { - if (mBufferReason != RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK - || mSelectedTracks.isEmpty()) { + if (mBufferReason != RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK) { return; } - long currentStartPositionUs = Long.MAX_VALUE; - for (int trackIndex : mSelectedTracks) { - currentStartPositionUs = Math.min(currentStartPositionUs, - mReadIoStates[trackIndex].getStartPositionUs()); - } for (int i = 0; i < mTrackCount; ++i) { long evictEndPositionUs = Math.min(mBufferManager.getStartPositionUs(mIds.get(i)), - currentStartPositionUs); + mReadIoStates[i].getStartPositionUs()); mBufferManager.evictChunks(mIds.get(i), evictEndPositionUs); } } diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java index 75eac5a2..7b098f40 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java @@ -43,7 +43,6 @@ public class SampleQueue { if (sampleFromQueue == null) { return SampleSource.NOTHING_READ; } - sample.ensureSpaceForWrite(sampleFromQueue.size); sample.size = sampleFromQueue.size; sample.flags = sampleFromQueue.flags; sample.timeUs = sampleFromQueue.timeUs; diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java index 0b219b41..40c4ef95 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java @@ -19,7 +19,6 @@ package com.android.tv.tuner.exoplayer.buffer; import android.os.ConditionVariable; import android.support.annotation.NonNull; - import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; diff --git a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java index 9fe921b8..258a5cd0 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java @@ -17,23 +17,20 @@ package com.android.tv.tuner.exoplayer.buffer; import android.content.Context; +import android.media.MediaFormat; import android.os.AsyncTask; +import android.os.Looper; import android.provider.Settings; -import android.support.annotation.NonNull; import android.util.Pair; -import com.android.tv.common.SoftPreconditions; - import java.io.File; import java.util.ArrayList; -import java.util.List; import java.util.SortedMap; /** * Manages Trickplay storage. */ public class TrickplayStorageManager implements BufferManager.StorageManager { - // TODO: Support multi-sessions. private static final String BUFFER_DIR = "timeshift"; // Copied from android.provider.Settings.Global (hidden fields) @@ -46,68 +43,53 @@ public class TrickplayStorageManager implements BufferManager.StorageManager { private static final int DEFAULT_THRESHOLD_PERCENTAGE = 10; private static final long DEFAULT_THRESHOLD_MAX_BYTES = 500L * 1024 * 1024; - private static AsyncTask<Void, Void, Void> sLastCacheCleanUpTask; - private static File sBufferDir; - private static long sStorageBufferBytes; - + private final File mBufferDir; private final long mMaxBufferSize; + private final long mStorageBufferBytes; - private static void initParamsIfNeeded(Context context, @NonNull File path) { - // TODO: Support multi-sessions. - SoftPreconditions.checkState( - sBufferDir == null || sBufferDir.equals(path)); - if (path.equals(sBufferDir)) { - return; - } - sBufferDir = path; + private static long getStorageBufferBytes(Context context, File path) { long lowPercentage = Settings.Global.getInt(context.getContentResolver(), SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE); - long lowPercentageToBytes = path.getTotalSpace() * lowPercentage / 100; + long lowBytes = path.getTotalSpace() * lowPercentage / 100; long maxLowBytes = Settings.Global.getLong(context.getContentResolver(), SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES); - sStorageBufferBytes = Math.min(lowPercentageToBytes, maxLowBytes); + return Math.min(lowBytes, maxLowBytes); } - public TrickplayStorageManager(Context context, @NonNull File baseDir, long maxBufferSize) { - initParamsIfNeeded(context, new File(baseDir, BUFFER_DIR)); - sBufferDir.mkdirs(); + public TrickplayStorageManager(Context context, File baseDir, long maxBufferSize) { + mBufferDir = new File(baseDir, BUFFER_DIR); + mBufferDir.mkdirs(); mMaxBufferSize = maxBufferSize; clearStorage(); + mStorageBufferBytes = getStorageBufferBytes(context, mBufferDir); } - private void clearStorage() { - long now = System.currentTimeMillis(); - if (sLastCacheCleanUpTask != null) { - sLastCacheCleanUpTask.cancel(true); + @Override + public void clearStorage() { + File files[] = mBufferDir.listFiles(); + if (files == null || files.length == 0) { + return; } - sLastCacheCleanUpTask = new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - if (isCancelled()) { - return null; - } - File files[] = sBufferDir.listFiles(); - if (files == null || files.length == 0) { - return null; - } - for (File file : files) { - if (isCancelled()) { - break; - } - long lastModified = file.lastModified(); - if (lastModified != 0 && lastModified < now) { + if (Looper.myLooper() == Looper.getMainLooper()) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + for (File file : files) { file.delete(); } + return null; } - return null; + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + for (File file : files) { + file.delete(); } - }; - sLastCacheCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } } @Override public File getBufferDir() { - return sBufferDir; + return mBufferDir; } @Override @@ -122,26 +104,25 @@ public class TrickplayStorageManager implements BufferManager.StorageManager { @Override public boolean hasEnoughBuffer(long pendingDelete) { - return sBufferDir.getUsableSpace() + pendingDelete >= sStorageBufferBytes; + return mBufferDir.getUsableSpace() + pendingDelete >= mStorageBufferBytes; } @Override - public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) { + public Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) { return null; } @Override - public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId) { + public ArrayList<Long> readIndexFile(String trackId) { return null; } @Override - public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio) { + public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio) { } @Override - public void writeIndexFile(String trackName, - SortedMap<Long, Pair<SampleChunk, Integer>> index) { + public void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index) { } } diff --git a/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java b/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java index 53678a85..97d9ece3 100644 --- a/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java +++ b/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java @@ -36,24 +36,6 @@ public class ConnectionTypeFragment extends SetupMultiPaneFragment { "com.android.tv.tuner.setup.ConnectionTypeFragment"; @Override - public void onCreate(Bundle savedInstanceState) { - ((TunerSetupActivity) getActivity()).generateTunerHal(); - super.onCreate(savedInstanceState); - } - - @Override - public void onResume() { - ((TunerSetupActivity) getActivity()).generateTunerHal(); - super.onResume(); - } - - @Override - public void onDestroy() { - ((TunerSetupActivity) getActivity()).clearTunerHal(); - super.onDestroy(); - } - - @Override protected SetupGuidedStepFragment onCreateContentFragment() { return new ContentFragment(); } diff --git a/src/com/android/tv/tuner/setup/PostalCodeFragment.java b/src/com/android/tv/tuner/setup/PostalCodeFragment.java deleted file mode 100644 index a4dd494c..00000000 --- a/src/com/android/tv/tuner/setup/PostalCodeFragment.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.setup; - -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v17.leanback.widget.GuidanceStylist.Guidance; -import android.support.v17.leanback.widget.GuidedAction; -import android.support.v17.leanback.widget.GuidedActionsStylist; -import android.text.InputFilter; -import android.text.Spanned; -import android.text.TextUtils; -import android.view.View; -import android.widget.TextView; -import android.widget.Toast; - -import com.android.tv.R; -import com.android.tv.common.ui.setup.SetupGuidedStepFragment; -import com.android.tv.common.ui.setup.SetupMultiPaneFragment; -import com.android.tv.tuner.util.PostalCodeUtils; - -import java.util.List; - -/** - * A fragment for initial screen. - */ -public class PostalCodeFragment extends SetupMultiPaneFragment { - public static final String ACTION_CATEGORY = - "com.android.tv.tuner.setup.PostalCodeFragment"; - private static final int VIEW_TYPE_EDITABLE = 1; - - @Override - protected SetupGuidedStepFragment onCreateContentFragment() { - ContentFragment fragment = new ContentFragment(); - Bundle arguments = new Bundle(); - arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true); - fragment.setArguments(arguments); - return fragment; - } - - @Override - protected String getActionCategory() { - return ACTION_CATEGORY; - } - - @Override - protected boolean needsDoneButton() { - return true; - } - - @Override - protected boolean needsSkipButton() { - return true; - } - - @Override - protected void setOnClickAction(View view, final String category, final int actionId) { - if (actionId == ACTION_DONE) { - view.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - CharSequence postalCode = - ((ContentFragment) getContentFragment()).mEditAction.getTitle(); - if (postalCode != null && postalCode.length() == 5) { - PostalCodeUtils.setLastPostalCode(getContext(), postalCode.toString()); - onActionClick(category, actionId); - } else { - ContentFragment contentFragment = (ContentFragment) getContentFragment(); - contentFragment.mEditAction.setDescription( - getString(R.string.postal_code_invalid_warning)); - contentFragment.notifyActionChanged(0); - contentFragment.mEditedActionView.performClick(); - } - } - }); - } else if (actionId == ACTION_SKIP) { - super.setOnClickAction(view, category, ACTION_SKIP); - } - } - - public static class ContentFragment extends SetupGuidedStepFragment { - private GuidedAction mEditAction; - private View mEditedActionView; - private View mDoneActionView; - private boolean mProceed; - - @Override - public void onGuidedActionFocused(GuidedAction action) { - if (action.equals(mEditAction)) { - if (mProceed) { - // "NEXT" in IME was just clicked, moves focus to Done button. - if (mDoneActionView == null) { - mDoneActionView = getActivity().findViewById(R.id.button_done); - } - mDoneActionView.requestFocus(); - mProceed = false; - } else { - // Directly opens IME to input postal/zip code. - if (mEditedActionView == null) { - mEditedActionView = getView().findViewById(R.id.guidedactions_editable); - ((TextView) mEditedActionView.findViewById(R.id.guidedactions_item_title)) - .setFilters(new InputFilter[]{new InputFilter() { - @Override - public CharSequence filter(CharSequence source, int start, - int end, Spanned dest, int dstart, int dend) { - try { - Integer.parseInt(source.toString()); - return null; - } catch (NumberFormatException e) { - return ""; - } - } - }, new InputFilter.LengthFilter(5)}); - } - mEditedActionView.performClick(); - } - } - } - - @Override - public long onGuidedActionEditedAndProceed(GuidedAction action) { - mProceed = true; - return 0; - } - - @NonNull - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - String title = getString(R.string.postal_code_guidance_title); - String description = getString(R.string.postal_code_guidance_description); - String breadcrumb = getString(R.string.ut_setup_breadcrumb); - return new Guidance(title, description, breadcrumb, null); - } - - @Override - public void onCreateActions(@NonNull List<GuidedAction> actions, - Bundle savedInstanceState) { - String description = getString(R.string.postal_code_action_description); - mEditAction = new GuidedAction.Builder(getActivity()).id(0).editable(true) - .description(description).build(); - actions.add(mEditAction); - } - - @Override - protected String getActionCategory() { - return ACTION_CATEGORY; - } - - @Override - public GuidedActionsStylist onCreateActionsStylist() { - return new GuidedActionsStylist() { - @Override - public int getItemViewType(GuidedAction action) { - if (action.isEditable()) { - return VIEW_TYPE_EDITABLE; - } - return super.getItemViewType(action); - } - - @Override - public int onProvideItemLayoutId(int viewType) { - if (viewType == VIEW_TYPE_EDITABLE) { - return R.layout.guided_action_editable; - } - return super.onProvideItemLayoutId(viewType); - } - }; - } - } -}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/setup/ScanFragment.java b/src/com/android/tv/tuner/setup/ScanFragment.java index 75b28e32..3b61debb 100644 --- a/src/com/android/tv/tuner/setup/ScanFragment.java +++ b/src/com/android/tv/tuner/setup/ScanFragment.java @@ -21,7 +21,6 @@ import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; import android.os.AsyncTask; -import android.os.Build; import android.os.Bundle; import android.os.ConditionVariable; import android.os.Handler; @@ -36,13 +35,14 @@ import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; +import com.android.tv.common.AutoCloseableUtils; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.ui.setup.SetupFragment; import com.android.tv.tuner.ChannelScanFileParser; -import com.android.tv.tuner.R; import com.android.tv.tuner.TunerHal; +import com.android.tv.tuner.R; import com.android.tv.tuner.TunerPreferences; -import com.android.tv.tuner.data.Channel; +import com.android.tv.tuner.data.nano.Channel; import com.android.tv.tuner.data.PsipData; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.source.FileTsStreamer; @@ -51,6 +51,7 @@ import com.android.tv.tuner.source.TsStreamer; import com.android.tv.tuner.source.TunerTsStreamer; import com.android.tv.tuner.tvinput.ChannelDataManager; import com.android.tv.tuner.tvinput.EventDetector; +import com.android.tv.tuner.util.TunerInputInfoUtils; import junit.framework.Assert; @@ -66,7 +67,6 @@ import java.util.concurrent.TimeUnit; public class ScanFragment extends SetupFragment { private static final String TAG = "ScanFragment"; private static final boolean DEBUG = false; - // In the fake mode, the connection to antenna or cable is not necessary. // Instead dummy channels are added. private static final boolean FAKE_MODE = false; @@ -98,7 +98,6 @@ public class ScanFragment extends SetupFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreateView"); View view = super.onCreateView(inflater, container, savedInstanceState); mChannelDataManager = new ChannelDataManager(getActivity()); mChannelDataManager.checkDataVersion(getActivity()); @@ -121,19 +120,13 @@ public class ScanFragment extends SetupFragment { } }); Bundle args = getArguments(); - int tunerType = (args == null ? 0 : args.getInt(TunerSetupActivity.KEY_TUNER_TYPE, 0)); // TODO: Handle the case when the fragment is restored. startScan(args == null ? 0 : args.getInt(EXTRA_FOR_CHANNEL_SCAN_FILE, 0)); TextView scanTitleView = (TextView) view.findViewById(R.id.tune_title); - switch (tunerType) { - case TunerHal.TUNER_TYPE_USB: - scanTitleView.setText(R.string.ut_channel_scan); - break; - case TunerHal.TUNER_TYPE_NETWORK: - scanTitleView.setText(R.string.nt_channel_scan); - break; - default: - scanTitleView.setText(R.string.bt_channel_scan); + if (TunerInputInfoUtils.isBuiltInTuner(getActivity())){ + scanTitleView.setText(R.string.bt_channel_scan); + } else { + scanTitleView.setText(R.string.ut_channel_scan); } return view; } @@ -154,14 +147,12 @@ public class ScanFragment extends SetupFragment { } @Override - public void onPause() { - Log.d(TAG, "onPause"); + public void onDetach() { if (mChannelScanTask != null) { // Ensure scan task will stop. - Log.w(TAG, "The activity went to the background. Stopping channel scan."); mChannelScanTask.stopScan(); } - super.onPause(); + super.onDetach(); } /** @@ -177,9 +168,7 @@ public class ScanFragment extends SetupFragment { new Handler().postDelayed(new Runnable() { @Override public void run() { - if (mChannelScanTask != null) { - mChannelScanTask.showFinishingProgressDialog(); - } + mChannelScanTask.showFinishingProgressDialog(); } }, SHOW_PROGRESS_DIALOG_DELAY_MS); @@ -266,7 +255,7 @@ public class ScanFragment extends SetupFragment { if (FAKE_MODE) { mScanTsStreamer = new FakeTsStreamer(this); } else { - TunerHal hal = ((TunerSetupActivity) mActivity).getTunerHal(); + TunerHal hal = TunerHal.createInstance(mActivity.getApplicationContext()); if (hal == null) { throw new RuntimeException("Failed to open a DVB device"); } @@ -327,17 +316,10 @@ public class ScanFragment extends SetupFragment { @Override protected void onProgressUpdate(Integer... values) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mProgressBar.setProgress(values[0], true); - } else { - mProgressBar.setProgress(values[0]); - } + mProgressBar.setProgress(values[0]); } private void stopScan() { - if (mLatch != null) { - mLatch.countDown(); - } mConditionStopped.open(); } @@ -378,7 +360,11 @@ public class ScanFragment extends SetupFragment { if (mConditionStopped.block(-1)) { break; } - publishProgress(MAX_PROGRESS * i++ / mScanChannelList.size()); + onProgressUpdate(MAX_PROGRESS * i++ / mScanChannelList.size()); + } + if (mScanTsStreamer instanceof TunerTsStreamer) { + AutoCloseableUtils.closeQuietly( + ((TunerTsStreamer) mScanTsStreamer).getTunerHal()); } mChannelDataManager.notifyScanCompleted(); if (!mConditionStopped.block(-1)) { @@ -468,13 +454,7 @@ public class ScanFragment extends SetupFragment { if (mFinishingProgressDialog != null) { mFinishingProgressDialog.dismiss(); } - // If the fragment is not resumed, the next fragment (scan result page) can't be - // displayed. In that case, just close the activity. - if (isResumed()) { - onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH); - } else if (getActivity() != null) { - getActivity().finish(); - } + onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH); mChannelScanTask = null; } } diff --git a/src/com/android/tv/tuner/setup/ScanResultFragment.java b/src/com/android/tv/tuner/setup/ScanResultFragment.java index 3b8cd823..068543cd 100644 --- a/src/com/android/tv/tuner/setup/ScanResultFragment.java +++ b/src/com/android/tv/tuner/setup/ScanResultFragment.java @@ -26,7 +26,6 @@ import android.support.v17.leanback.widget.GuidedAction; import com.android.tv.common.ui.setup.SetupGuidedStepFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.tuner.R; -import com.android.tv.tuner.TunerHal; import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.util.TunerInputInfoUtils; @@ -77,19 +76,11 @@ public class ScanResultFragment extends SetupMultiPaneFragment { mChannelCountOnPreference, mChannelCountOnPreference); breadcrumb = null; } else { - Bundle args = getArguments(); - int tunerType = - (args == null ? 0 : args.getInt(TunerSetupActivity.KEY_TUNER_TYPE, 0)); title = getString(R.string.ut_result_not_found_title); - switch (tunerType) { - case TunerHal.TUNER_TYPE_USB: - description = getString(R.string.ut_result_not_found_description); - break; - case TunerHal.TUNER_TYPE_NETWORK: - description = getString(R.string.nt_result_not_found_description); - break; - default: - description = getString(R.string.bt_result_not_found_description); + if (TunerInputInfoUtils.isBuiltInTuner(getActivity())) { + description = getString(R.string.bt_result_not_found_description); + } else { + description = getString(R.string.ut_result_not_found_description); } breadcrumb = getString(R.string.ut_setup_breadcrumb); } diff --git a/src/com/android/tv/tuner/setup/TunerSetupActivity.java b/src/com/android/tv/tuner/setup/TunerSetupActivity.java index f618c699..78121bc5 100644 --- a/src/com/android/tv/tuner/setup/TunerSetupActivity.java +++ b/src/com/android/tv/tuner/setup/TunerSetupActivity.java @@ -29,53 +29,35 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.tv.TvContract; -import android.os.AsyncTask; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; import android.support.v4.app.NotificationCompat; -import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import android.widget.Toast; import com.android.tv.TvApplication; -import com.android.tv.common.AutoCloseableUtils; import com.android.tv.common.TvCommonConstants; import com.android.tv.common.TvCommonUtils; import com.android.tv.common.ui.setup.SetupActivity; import com.android.tv.common.ui.setup.SetupFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; -import com.android.tv.experiments.Experiments; import com.android.tv.tuner.R; import com.android.tv.tuner.TunerHal; import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.tvinput.TunerTvInputService; -import com.android.tv.tuner.util.PostalCodeUtils; -import com.android.tv.util.LocationUtils; - -import java.util.Locale; -import java.util.concurrent.Executor; +import com.android.tv.tuner.util.TunerInputInfoUtils; /** * An activity that serves tuner setup process. */ public class TunerSetupActivity extends SetupActivity { - private static final String TAG = "TunerSetupActivity"; - private static final boolean DEBUG = false; - - /** - * Key for passing tuner type to sub-fragments. - */ - public static final String KEY_TUNER_TYPE = "TunerSetupActivity.tunerType"; - + private final String TAG = "TunerSetupActivity"; // For the recommendation card private static final String TV_ACTIVITY_CLASS_NAME = "com.android.tv.TvActivity"; private static final String NOTIFY_TAG = "TunerSetup"; private static final int NOTIFY_ID = 1000; private static final String TAG_DRAWABLE = "drawable"; private static final String TAG_ICON = "ic_launcher_s"; - private static final int PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION = 1; private static final int CHANNEL_MAP_SCAN_FILE[] = { R.raw.ut_us_atsc_center_frequencies_8vsb, @@ -87,13 +69,9 @@ public class TunerSetupActivity extends SetupActivity { R.raw.ut_kr_dev_cj_cable_center_frequencies_qam256}; private ScanFragment mLastScanFragment; - private Integer mTunerType; - private TunerHalFactory mTunerHalFactory; - private boolean mNeedToShowPostalCodeFragment; @Override protected void onCreate(Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreate"); TvApplication.setCurrentRunningProcess(this, false); super.onCreate(savedInstanceState); // TODO: check {@link shouldShowRequestPermissionRationale}. @@ -101,49 +79,13 @@ public class TunerSetupActivity extends SetupActivity { != PackageManager.PERMISSION_GRANTED) { // No need to check the request result. requestPermissions(new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION}, - PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION); - } - mTunerType = TunerHal.getTunerTypeAndCount(this).first; - if (mTunerType == null) { - finish(); - } else { - mTunerHalFactory = new TunerHalFactory(getApplicationContext()); - } - try { - // Updating postal code takes time, therefore we called it here for "warm-up". - PostalCodeUtils.setLastPostalCode(this, null); - PostalCodeUtils.updatePostalCode(this); - } catch (Exception e) { - // Do nothing. If the last known postal code is null, we'll show guided fragment to - // prompt users to input postal code before ConnectionTypeFragment is shown. - Log.i(TAG, "Can't get postal code:" + e); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { - if (requestCode == PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED - && Experiments.CLOUD_EPG.get()) { - try { - // Updating postal code takes time, therefore we should update postal code - // right after the permission is granted, so that the subsequent operations, - // especially EPG fetcher, could get the newly updated postal code. - PostalCodeUtils.updatePostalCode(this); - } catch (Exception e) { - // Do nothing - } - } + 0); } } @Override protected Fragment onCreateInitialFragment() { SetupFragment fragment = new WelcomeFragment(); - Bundle args = new Bundle(); - args.putInt(KEY_TUNER_TYPE, mTunerType); - fragment.setArguments(args); fragment.setShortDistance(SetupFragment.FRAGMENT_EXIT_TRANSITION | SetupFragment.FRAGMENT_REENTER_TRANSITION); return fragment; @@ -160,41 +102,33 @@ public class TunerSetupActivity extends SetupActivity { finish(); break; default: { - if (mNeedToShowPostalCodeFragment - || Locale.US.getCountry().equalsIgnoreCase( - LocationUtils.getCurrentCountry(getApplicationContext())) - && TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(this))) { - // We cannot get postal code automatically. Postal code input fragment - // should always be shown even if users have input some valid postal - // code in this activity before. - mNeedToShowPostalCodeFragment = true; - showPostalCodeFragment(); - } else { - showConnectionTypeFragment(); - } + SetupFragment fragment = new ConnectionTypeFragment(); + fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION + | SetupFragment.FRAGMENT_RETURN_TRANSITION); + showFragment(fragment, true); break; } } return true; - case PostalCodeFragment.ACTION_CATEGORY: - if (actionId == SetupMultiPaneFragment.ACTION_DONE - || actionId == SetupMultiPaneFragment.ACTION_SKIP) { - showConnectionTypeFragment(); - } - return true; case ConnectionTypeFragment.ACTION_CATEGORY: - if (mTunerHalFactory.get() == null) { + TunerHal hal = TunerHal.createInstance(getApplicationContext()); + if (hal == null) { finish(); Toast.makeText(getApplicationContext(), R.string.ut_channel_scan_tuner_unavailable,Toast.LENGTH_LONG).show(); return true; } + try { + hal.close(); + } catch (Exception e) { + Log.e(TAG, "Tuner hal close failed", e); + return true; + } mLastScanFragment = new ScanFragment(); - Bundle args1 = new Bundle(); - args1.putInt(ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE, + Bundle args = new Bundle(); + args.putInt(ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE, CHANNEL_MAP_SCAN_FILE[actionId]); - args1.putInt(KEY_TUNER_TYPE, mTunerType); - mLastScanFragment.setArguments(args1); + mLastScanFragment.setArguments(args); showFragment(mLastScanFragment, true); return true; case ScanFragment.ACTION_CATEGORY: @@ -203,11 +137,7 @@ public class TunerSetupActivity extends SetupActivity { getFragmentManager().popBackStack(); return true; case ScanFragment.ACTION_FINISH: - mTunerHalFactory.clear(); SetupFragment fragment = new ScanResultFragment(); - Bundle args2 = new Bundle(); - args2.putInt(KEY_TUNER_TYPE, mTunerType); - fragment.setArguments(args2); fragment.setShortDistance(SetupFragment.FRAGMENT_EXIT_TRANSITION | SetupFragment.FRAGMENT_REENTER_TRANSITION); showFragment(fragment, true); @@ -283,7 +213,7 @@ public class TunerSetupActivity extends SetupActivity { String inputId = TvContract.buildInputId(new ComponentName(context.getPackageName(), TunerTvInputService.class.getName())); - // Make an intent to launch the setup activity of TV tuner input. + // Make an intent to launch the setup activity of USB tuner TV input. Intent intent = TvCommonUtils.createSetupIntent( new Intent(context, TunerSetupActivity.class), inputId); intent.putExtra(TvCommonConstants.EXTRA_INPUT_ID, inputId); @@ -294,27 +224,6 @@ public class TunerSetupActivity extends SetupActivity { } /** - * Gets the currently used tuner HAL. - */ - TunerHal getTunerHal() { - return mTunerHalFactory.get(); - } - - /** - * Generates tuner HAL. - */ - void generateTunerHal() { - mTunerHalFactory.generate(); - } - - /** - * Clears the currently used tuner HAL. - */ - void clearTunerHal() { - mTunerHalFactory.clear(); - } - - /** * Returns a {@link PendingIntent} to launch the tuner TV input service. * * @param context a {@link Context} instance @@ -333,19 +242,12 @@ public class TunerSetupActivity extends SetupActivity { Resources resources = context.getResources(); String focusedTitle = resources.getString( R.string.ut_setup_recommendation_card_focused_title); - int titleStringId = 0; - switch (TunerHal.getTunerTypeAndCount(context).first) { - case TunerHal.TUNER_TYPE_BUILT_IN: - titleStringId = R.string.bt_setup_recommendation_card_title; - break; - case TunerHal.TUNER_TYPE_USB: - titleStringId = R.string.ut_setup_recommendation_card_title; - break; - case TunerHal.TUNER_TYPE_NETWORK: - titleStringId = R.string.nt_setup_recommendation_card_title; - break; + String title; + if (TunerInputInfoUtils.isBuiltInTuner(context)) { + title = resources.getString(R.string.bt_setup_recommendation_card_title); + } else { + title = resources.getString(R.string.ut_setup_recommendation_card_title); } - String title = resources.getString(titleStringId); Bitmap largeIcon = BitmapFactory.decodeResource(resources, R.drawable.recommendation_antenna); @@ -367,20 +269,6 @@ public class TunerSetupActivity extends SetupActivity { notificationManager.notify(NOTIFY_TAG, NOTIFY_ID, notification); } - private void showPostalCodeFragment() { - SetupFragment fragment = new PostalCodeFragment(); - fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION - | SetupFragment.FRAGMENT_RETURN_TRANSITION); - showFragment(fragment, true); - } - - private void showConnectionTypeFragment() { - SetupFragment fragment = new ConnectionTypeFragment(); - fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION - | SetupFragment.FRAGMENT_RETURN_TRANSITION); - showFragment(fragment, true); - } - /** * Cancels the previously shown recommendation card. * @@ -391,80 +279,4 @@ public class TunerSetupActivity extends SetupActivity { .getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(NOTIFY_TAG, NOTIFY_ID); } - - @VisibleForTesting - static class TunerHalFactory { - private Context mContext; - @VisibleForTesting - TunerHal mTunerHal; - private GenerateTunerHalTask mGenerateTunerHalTask; - private final Executor mExecutor; - - TunerHalFactory(Context context) { - this(context, AsyncTask.SERIAL_EXECUTOR); - } - - TunerHalFactory(Context context, Executor executor) { - mContext = context; - mExecutor = executor; - } - - /** - * Returns tuner HAL currently used. If it's {@code null} and tuner HAL is not generated - * before, tries to generate it synchronously. - */ - TunerHal get() { - if (mGenerateTunerHalTask != null - && mGenerateTunerHalTask.getStatus() != AsyncTask.Status.FINISHED) { - try { - return mGenerateTunerHalTask.get(); - } catch (Exception e) { - Log.e(TAG, "Cannot get Tuner HAL: " + e); - } - } else if (mGenerateTunerHalTask == null && mTunerHal == null) { - mTunerHal = createInstance(); - } - return mTunerHal; - } - - /** - * Generates tuner hal for scanning with asynchronous tasks. - */ - void generate() { - if (mGenerateTunerHalTask == null && mTunerHal == null) { - mGenerateTunerHalTask = new GenerateTunerHalTask(); - mGenerateTunerHalTask.executeOnExecutor(mExecutor); - } - } - - /** - * Clears the currently used tuner hal. - */ - void clear() { - if (mGenerateTunerHalTask != null) { - mGenerateTunerHalTask.cancel(true); - mGenerateTunerHalTask = null; - } - if (mTunerHal != null) { - AutoCloseableUtils.closeQuietly(mTunerHal); - mTunerHal = null; - } - } - - protected TunerHal createInstance() { - return TunerHal.createInstance(mContext); - } - - class GenerateTunerHalTask extends AsyncTask<Void, Void, TunerHal> { - @Override - protected TunerHal doInBackground(Void... args) { - return createInstance(); - } - - @Override - protected void onPostExecute(TunerHal tunerHal) { - mTunerHal = tunerHal; - } - } - } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/tuner/setup/WelcomeFragment.java b/src/com/android/tv/tuner/setup/WelcomeFragment.java index a3dddc72..7e809411 100644 --- a/src/com/android/tv/tuner/setup/WelcomeFragment.java +++ b/src/com/android/tv/tuner/setup/WelcomeFragment.java @@ -27,7 +27,6 @@ import android.view.ViewGroup; import com.android.tv.common.ui.setup.SetupGuidedStepFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.tuner.R; -import com.android.tv.tuner.TunerHal; import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.util.TunerInputInfoUtils; @@ -42,9 +41,7 @@ public class WelcomeFragment extends SetupMultiPaneFragment { @Override protected SetupGuidedStepFragment onCreateContentFragment() { - ContentFragment fragment = new ContentFragment(); - fragment.setArguments(getArguments()); - return fragment; + return new ContentFragment(); } @Override @@ -73,33 +70,20 @@ public class WelcomeFragment extends SetupMultiPaneFragment { public Guidance onCreateGuidance(Bundle savedInstanceState) { String title; String description; - int tunerType = getArguments().getInt(TunerSetupActivity.KEY_TUNER_TYPE, - TunerHal.TUNER_TYPE_BUILT_IN); if (mChannelCountOnPreference == 0) { - switch (tunerType) { - case TunerHal.TUNER_TYPE_USB: - title = getString(R.string.ut_setup_new_title); - description = getString(R.string.ut_setup_new_description); - break; - case TunerHal.TUNER_TYPE_NETWORK: - title = getString(R.string.nt_setup_new_title); - description = getString(R.string.nt_setup_new_description); - break; - default: - title = getString(R.string.bt_setup_new_title); - description = getString(R.string.bt_setup_new_description); + if (TunerInputInfoUtils.isBuiltInTuner(getActivity())) { + title = getString(R.string.bt_setup_new_title); + description = getString(R.string.bt_setup_new_description); + } else { + title = getString(R.string.ut_setup_new_title); + description = getString(R.string.ut_setup_new_description); } } else { title = getString(R.string.bt_setup_again_title); - switch (tunerType) { - case TunerHal.TUNER_TYPE_USB: - description = getString(R.string.ut_setup_again_description); - break; - case TunerHal.TUNER_TYPE_NETWORK: - description = getString(R.string.nt_setup_again_description); - break; - default: - description = getString(R.string.bt_setup_again_description); + if (TunerInputInfoUtils.isBuiltInTuner(getActivity())) { + description = getString(R.string.bt_setup_again_description); + } else { + description = getString(R.string.ut_setup_again_description); } } return new Guidance(title, description, null, null); diff --git a/src/com/android/tv/tuner/source/FileTsStreamer.java b/src/com/android/tv/tuner/source/FileTsStreamer.java index 80ec8a56..14997ee4 100644 --- a/src/com/android/tv/tuner/source/FileTsStreamer.java +++ b/src/com/android/tv/tuner/source/FileTsStreamer.java @@ -256,7 +256,7 @@ public class FileTsStreamer implements TsStreamer { * Returns whether the current pid filter is empty or not. */ public boolean isFilterEmpty() { - return mPids.size() == 0; + return mPids.size() > 0; } /** diff --git a/src/com/android/tv/tuner/source/TsDataSourceManager.java b/src/com/android/tv/tuner/source/TsDataSourceManager.java index 32504b95..ccbb75ba 100644 --- a/src/com/android/tv/tuner/source/TsDataSourceManager.java +++ b/src/com/android/tv/tuner/source/TsDataSourceManager.java @@ -17,10 +17,8 @@ package com.android.tv.tuner.source; import android.content.Context; -import android.support.annotation.VisibleForTesting; -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.data.Channel; +import com.android.tv.tuner.data.nano.Channel; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.tvinput.EventDetector; @@ -129,14 +127,6 @@ public class TsDataSourceManager { } /** - * Add tuner hal into TunerTsStreamerManager for test. - */ - @VisibleForTesting - public void addTunerHalForTest(TunerHal tunerHal) { - mTunerStreamerManager.addTunerHal(tunerHal, mId); - } - - /** * Releases persistent resources. */ public void release() { diff --git a/src/com/android/tv/tuner/source/TunerTsStreamer.java b/src/com/android/tv/tuner/source/TunerTsStreamer.java index 65b11a5a..b24048e6 100644 --- a/src/com/android/tv/tuner/source/TunerTsStreamer.java +++ b/src/com/android/tv/tuner/source/TunerTsStreamer.java @@ -42,17 +42,15 @@ public class TunerTsStreamer implements TsStreamer { private static final int MIN_READ_UNIT = 1500; private static final int READ_BUFFER_SIZE = MIN_READ_UNIT * 10; // ~15KB private static final int CIRCULAR_BUFFER_SIZE = MIN_READ_UNIT * 20000; // ~ 30MB - private static final int TS_PACKET_SIZE = 188; private static final int READ_TIMEOUT_MS = 5000; // 5 secs. private static final int BUFFER_UNDERRUN_SLEEP_MS = 10; - private static final int READ_ERROR_STREAMING_ENDED = -1; - private static final int READ_ERROR_BUFFER_OVERWRITTEN = -2; private final Object mCircularBufferMonitor = new Object(); private final byte[] mCircularBuffer = new byte[CIRCULAR_BUFFER_SIZE]; private long mBytesFetched; private final AtomicLong mLastReadPosition = new AtomicLong(); + private boolean mEndOfStreamSent; private boolean mStreaming; private final TunerHal mTunerHal; @@ -61,7 +59,6 @@ public class TunerTsStreamer implements TsStreamer { private final EventDetector mEventDetector; private final TsStreamWriter mTsStreamWriter; - private String mChannelNumber; public static class TunerDataSource extends TsDataSource { private final TunerTsStreamer mTsStreamer; @@ -106,15 +103,6 @@ public class TunerTsStreamer implements TsStreamer { offset, readLength); if (ret > 0) { mLastReadPosition.addAndGet(ret); - } else if (ret == READ_ERROR_BUFFER_OVERWRITTEN) { - long currentPosition = mStartBufferedPosition + mLastReadPosition.get(); - long endPosition = mTsStreamer.getBufferedPosition(); - long diff = ((endPosition - currentPosition + TS_PACKET_SIZE - 1) / TS_PACKET_SIZE) - * TS_PACKET_SIZE; - Log.w(TAG, "Demux position jump by overwritten buffer: " + diff); - mStartBufferedPosition = currentPosition + diff; - mLastReadPosition.set(0); - return 0; } return ret; } @@ -126,10 +114,7 @@ public class TunerTsStreamer implements TsStreamer { */ public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener, Context context) { mTunerHal = tunerHal; - mEventDetector = new EventDetector(mTunerHal); - if (eventListener != null) { - mEventDetector.registerListener(eventListener); - } + mEventDetector = new EventDetector(mTunerHal, eventListener); mTsStreamWriter = context != null && TunerPreferences.getStoreTsStream(context) ? new TsStreamWriter(context) : null; } @@ -140,8 +125,7 @@ public class TunerTsStreamer implements TsStreamer { @Override public boolean startStream(TunerChannel channel) { - if (mTunerHal.tune(channel.getFrequency(), channel.getModulation(), - channel.getDisplayNumber(false))) { + if (mTunerHal.tune(channel.getFrequency(), channel.getModulation())) { if (channel.hasVideo()) { mTunerHal.addPidFilter(channel.getVideoPid(), TunerHal.FILTER_TYPE_VIDEO); @@ -164,7 +148,6 @@ public class TunerTsStreamer implements TsStreamer { channel.getProgramNumber()); } mChannel = channel; - mChannelNumber = channel.getDisplayNumber(); synchronized (mCircularBufferMonitor) { if (mStreaming) { Log.w(TAG, "Streaming should be stopped before start streaming"); @@ -173,6 +156,7 @@ public class TunerTsStreamer implements TsStreamer { mStreaming = true; mBytesFetched = 0; mLastReadPosition.set(0L); + mEndOfStreamSent = false; } if (mTsStreamWriter != null) { mTsStreamWriter.setChannel(mChannel); @@ -188,7 +172,7 @@ public class TunerTsStreamer implements TsStreamer { @Override public boolean startStream(ChannelScanFileParser.ScanChannel channel) { - if (mTunerHal.tune(channel.frequency, channel.modulation, null)) { + if (mTunerHal.tune(channel.frequency, channel.modulation)) { mEventDetector.startDetecting( channel.frequency, channel.modulation, EventDetector.ALL_PROGRAM_NUMBERS); synchronized (mCircularBufferMonitor) { @@ -199,6 +183,7 @@ public class TunerTsStreamer implements TsStreamer { mStreaming = true; mBytesFetched = 0; mLastReadPosition.set(0L); + mEndOfStreamSent = false; } mStreamingThread = new StreamingThread(); mStreamingThread.start(); @@ -273,22 +258,6 @@ public class TunerTsStreamer implements TsStreamer { } } - public String getStreamerInfo() { - return "Channel: " + mChannelNumber + ", Streaming: " + mStreaming; - } - - public void registerListener(EventListener listener) { - if (mEventDetector != null && listener != null) { - mEventDetector.registerListener(listener); - } - } - - public void unregisterListener(EventListener listener) { - if (mEventDetector != null) { - mEventDetector.unregisterListener(listener); - } - } - private class StreamingThread extends Thread { @Override public void run() { @@ -352,14 +321,21 @@ public class TunerTsStreamer implements TsStreamer { * @throws IOException */ public int readAt(long pos, byte[] buffer, int offset, int amount) throws IOException { + long readStartTime = System.currentTimeMillis(); while (true) { synchronized (mCircularBufferMonitor) { - if (!mStreaming) { - return READ_ERROR_STREAMING_ENDED; + if (mEndOfStreamSent || !mStreaming) { + return -1; } if (mBytesFetched - CIRCULAR_BUFFER_SIZE > pos) { - Log.w(TAG, "Demux is requesting the data which is already overwritten."); - return READ_ERROR_BUFFER_OVERWRITTEN; + Log.e(TAG, "Demux is requesting the data which is already overwritten."); + return -1; + } + if (System.currentTimeMillis() - readStartTime > READ_TIMEOUT_MS) { + // Nothing was received during READ_TIMEOUT_MS before. + mEndOfStreamSent = true; + mCircularBufferMonitor.notifyAll(); + return -1; } if (mBytesFetched < pos + amount) { try { diff --git a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java index fcd14116..cf1f6dcf 100644 --- a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java +++ b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java @@ -42,7 +42,6 @@ class TunerTsStreamerManager { private final Object mCancelLock = new Object(); private final StreamerFinder mStreamerFinder = new StreamerFinder(); private final Map<Integer, TsStreamerCreator> mCreators = new HashMap<>(); - private final Map<Integer, EventDetector.EventListener> mListeners = new HashMap<>(); private final Map<TsDataSource, TunerTsStreamer> mSourceToStreamerMap = new HashMap<>(); private final TunerHalManager mTunerHalManager = new TunerHalManager(); private static TunerTsStreamerManager sInstance; @@ -69,8 +68,6 @@ class TunerTsStreamerManager { mStreamerFinder.appendSessionLocked(channel, sessionId); TunerTsStreamer streamer = mStreamerFinder.getStreamerLocked(channel); TsDataSource source = streamer.createDataSource(); - mListeners.put(sessionId, listener); - streamer.registerListener(listener); mSourceToStreamerMap.put(source, streamer); return source; } @@ -86,8 +83,6 @@ class TunerTsStreamerManager { if (!creator.isCancelledLocked()) { mStreamerFinder.putLocked(channel, sessionId, streamer); TsDataSource source = streamer.createDataSource(); - mListeners.put(sessionId, listener); - streamer.registerListener(listener); mSourceToStreamerMap.put(source, streamer); return source; } @@ -109,8 +104,6 @@ class TunerTsStreamerManager { if (streamer == null) { return; } - EventDetector.EventListener listener = mListeners.remove(sessionId); - streamer.unregisterListener(listener); TunerChannel channel = streamer.getChannel(); SoftPreconditions.checkState(channel != null); mStreamerFinder.removeSessionLocked(channel, sessionId); @@ -132,13 +125,6 @@ class TunerTsStreamerManager { } } - /** - * Add tuner hal into TunerHalManager for test. - */ - void addTunerHal(TunerHal tunerHal, int sessionId) { - mTunerHalManager.addTunerHal(tunerHal, sessionId); - } - synchronized void release(int sessionId) { mTunerHalManager.releaseCachedHal(sessionId); } @@ -275,16 +261,16 @@ class TunerTsStreamerManager { } private void releaseTunerHal(TunerHal hal, int sessionId, boolean reuse) { - if (!reuse || !hal.isReusable()) { + if (!reuse) { AutoCloseableUtils.closeQuietly(hal); return; } TunerHal cachedHal = mTunerHals.get(sessionId); if (cachedHal != hal) { mTunerHals.put(sessionId, hal); - if (cachedHal != null) { - AutoCloseableUtils.closeQuietly(cachedHal); - } + } + if (cachedHal != null && cachedHal != hal) { + AutoCloseableUtils.closeQuietly(cachedHal); } } @@ -297,9 +283,5 @@ class TunerTsStreamerManager { AutoCloseableUtils.closeQuietly(hal); } } - - private void addTunerHal(TunerHal tunerHal, int sessionId) { - mTunerHals.put(sessionId, tunerHal); - } } }
\ No newline at end of file diff --git a/src/com/android/tv/tuner/ts/SectionParser.java b/src/com/android/tv/tuner/ts/SectionParser.java index fe972cd1..8c1f6a1b 100644 --- a/src/com/android/tv/tuner/ts/SectionParser.java +++ b/src/com/android/tv/tuner/ts/SectionParser.java @@ -22,7 +22,7 @@ import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; -import com.android.tv.tuner.data.Channel; +import com.android.tv.tuner.data.nano.Channel; import com.android.tv.tuner.data.PsiData.PatItem; import com.android.tv.tuner.data.PsiData.PmtItem; import com.android.tv.tuner.data.PsipData.Ac3AudioDescriptor; @@ -39,8 +39,8 @@ import com.android.tv.tuner.data.PsipData.RatingRegion; import com.android.tv.tuner.data.PsipData.RegionalRating; import com.android.tv.tuner.data.PsipData.TsDescriptor; import com.android.tv.tuner.data.PsipData.VctItem; -import com.android.tv.tuner.data.Track.AtscAudioTrack; -import com.android.tv.tuner.data.Track.AtscCaptionTrack; +import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; +import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; import com.android.tv.tuner.util.ByteArrayBuffer; import com.ibm.icu.text.UnicodeDecompressor; @@ -367,10 +367,6 @@ public class SectionParser { mParsedEttItems.clear(); } - public void resetVersionNumbers() { - mSectionVersionMap.clear(); - } - private void parseSection(byte[] data) { if (!checkSanity(data)) { Log.d(TAG, "Bad CRC!"); @@ -514,8 +510,10 @@ public class SectionParser { pos += 11 + descriptorsLength; results.add(new MgtItem(tableType, tableTypePid)); } - // Skip the remaining descriptor part which we don't use. - + if ((data[pos] & 0xf0) != 0xf0) { + Log.e(TAG, "Broken MGT."); + return false; + } if (mListener != null) { mListener.onMgtParsed(results); } @@ -719,9 +717,6 @@ public class SectionParser { if (audioDescriptor.getLanguage() != null) { audioTrack.language = audioDescriptor.getLanguage(); } - if (audioTrack.language == null) { - audioTrack.language = ""; - } audioTrack.audioType = AtscAudioTrack.AUDIOTYPE_UNDEFINED; audioTrack.channelCount = audioDescriptor.getNumChannels(); audioTrack.sampleRate = audioDescriptor.getSampleRate(); @@ -953,7 +948,6 @@ public class SectionParser { pos += 3; boolean ccType = (data[pos] & 0x80) != 0; if (!ccType) { - pos +=3; continue; } int captionServiceNumber = data[pos] & 0x3f; diff --git a/src/com/android/tv/tuner/ts/TsParser.java b/src/com/android/tv/tuner/ts/TsParser.java index 21b5a942..c24c2a21 100644 --- a/src/com/android/tv/tuner/ts/TsParser.java +++ b/src/com/android/tv/tuner/ts/TsParser.java @@ -102,7 +102,6 @@ public class TsParser { } protected abstract void handleData(byte[] data, boolean startIndicator); - protected abstract void resetDataVersions(); } private class SectionStream extends Stream { @@ -139,11 +138,6 @@ public class TsParser { mSectionParser.parseSections(mPacket); } - @Override - protected void resetDataVersions() { - mSectionParser.resetVersionNumbers(); - } - private final OutputListener mSectionListener = new OutputListener() { @Override public void onPatParsed(List<PatItem> items) { @@ -457,16 +451,4 @@ public class TsParser { } return incompleteChannels; } - - /** - * Reset the versions so that data with old version number can be handled. - */ - public void resetDataVersions() { - for (int eitPid : mEITPids) { - Stream stream = mStreamMap.get(eitPid); - if (stream != null) { - stream.resetDataVersions(); - } - } - } } diff --git a/src/com/android/tv/tuner/tvinput/ChannelDataManager.java b/src/com/android/tv/tuner/tvinput/ChannelDataManager.java index 885cef9f..a16bc522 100644 --- a/src/com/android/tv/tuner/tvinput/ChannelDataManager.java +++ b/src/com/android/tv/tuner/tvinput/ChannelDataManager.java @@ -30,7 +30,6 @@ import android.os.HandlerThread; import android.os.Message; import android.os.RemoteException; import android.support.annotation.Nullable; -import android.support.v4.os.BuildCompat; import android.text.format.DateUtils; import android.util.Log; @@ -38,7 +37,6 @@ import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.data.PsipData.EitItem; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.util.ConvertUtils; -import com.android.tv.util.PermissionUtils; import java.util.ArrayList; import java.util.Collections; @@ -194,14 +192,11 @@ public class ChannelDataManager implements Handler.Callback { public void release() { mHandler.removeCallbacksAndMessages(null); - releaseSafely(); + mHandlerThread.quitSafely(); } public void releaseSafely() { mHandlerThread.quitSafely(); - mListener = null; - mChannelScanListener = null; - mChannelScanHandler = null; } public TunerChannel getChannel(long channelId) { @@ -440,7 +435,7 @@ public class ChannelDataManager implements Handler.Callback { } } ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert( - TvContract.Programs.CONTENT_URI), newItem, channel)); + TvContract.Programs.CONTENT_URI), newItem, channel.getChannelId())); if (ops.size() >= BATCH_OPERATION_COUNT) { applyBatch(channel.getName(), ops); ops.clear(); @@ -510,7 +505,7 @@ public class ChannelDataManager implements Handler.Callback { continue; } ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert( - TvContract.Programs.CONTENT_URI), item, channel)); + TvContract.Programs.CONTENT_URI), item, channel.getChannelId())); if (ops.size() >= BATCH_OPERATION_COUNT) { applyBatch(channel.getName(), ops); ops.clear(); @@ -521,13 +516,9 @@ public class ChannelDataManager implements Handler.Callback { } private ContentProviderOperation buildContentProviderOperation( - ContentProviderOperation.Builder builder, EitItem item, TunerChannel channel) { - if (channel != null) { - builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channel.getChannelId()); - if (BuildCompat.isAtLeastN()) { - builder.withValue(TvContract.Programs.COLUMN_RECORDING_PROHIBITED, - channel.isRecordingProhibited() ? 1 : 0); - } + ContentProviderOperation.Builder builder, EitItem item, Long channelId) { + if (channelId != null) { + builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channelId); } if (item != null) { builder.withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText()) @@ -565,10 +556,7 @@ public class ChannelDataManager implements Handler.Callback { values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, channel.getName()); values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, channel.toByteArray()); values.put(TvContract.Channels.COLUMN_DESCRIPTION, channel.getDescription()); - values.put(TvContract.Channels.COLUMN_VIDEO_FORMAT, channel.getVideoFormat()); values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, VERSION); - values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, - channel.isRecordingProhibited() ? 1 : 0); if (channelId <= 0) { values.put(TvContract.Channels.COLUMN_INPUT_ID, mInputId); @@ -610,29 +598,13 @@ public class ChannelDataManager implements Handler.Callback { } private void checkVersion() { - if (PermissionUtils.hasAccessAllEpg(mContext)) { - String selection = TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + "<>?"; - try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, - CHANNEL_DATA_SELECTION_ARGS, selection, - new String[] {Integer.toString(VERSION)}, null)) { - if (cursor != null && cursor.moveToFirst()) { - // The stored channel data seem outdated. Delete them all. - clearChannels(); - } - } - } else { - try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, - new String[] { TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 }, - null, null, null)) { - if (cursor != null) { - while (cursor.moveToNext()) { - int version = cursor.getInt(0); - if (version != VERSION) { - clearChannels(); - break; - } - } - } + String selection = TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + "<>?"; + try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, + CHANNEL_DATA_SELECTION_ARGS, selection, + new String[] {Integer.toString(VERSION)}, null)) { + if (cursor != null && cursor.moveToFirst()) { + // The stored channel data seem outdated. Delete them all. + clearChannels(); } } } diff --git a/src/com/android/tv/tuner/tvinput/EventDetector.java b/src/com/android/tv/tuner/tvinput/EventDetector.java index 96b20a4b..a132398f 100644 --- a/src/com/android/tv/tuner/tvinput/EventDetector.java +++ b/src/com/android/tv/tuner/tvinput/EventDetector.java @@ -21,8 +21,8 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.data.Track.AtscAudioTrack; -import com.android.tv.tuner.data.Track.AtscCaptionTrack; +import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; +import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.ts.TsParser; import com.android.tv.tuner.data.PsiData; @@ -51,7 +51,7 @@ public class EventDetector { private final SparseArray<TunerChannel> mChannelMap = new SparseArray<>(); private final SparseBooleanArray mVctCaptionTracksFound = new SparseBooleanArray(); private final SparseBooleanArray mEitCaptionTracksFound = new SparseBooleanArray(); - private final List<EventListener> mEventListeners = new ArrayList<>(); + private final EventListener mEventListener; private int mFrequency; private String mModulation; private int mProgramNumber = ALL_PROGRAM_NUMBERS; @@ -105,10 +105,8 @@ public class EventDetector { item.setHasCaptionTrack(); } } - if (tunerChannel != null && !mEventListeners.isEmpty()) { - for (EventListener eventListener : mEventListeners) { - eventListener.onEventDetected(tunerChannel, items); - } + if (tunerChannel != null && mEventListener != null) { + mEventListener.onEventDetected(tunerChannel, items); } } @@ -119,10 +117,8 @@ public class EventDetector { @Override public void onAllVctItemsParsed() { - if (!mEventListeners.isEmpty()) { - for (EventListener eventListener : mEventListeners) { - eventListener.onChannelScanDone(); - } + if (mEventListener != null) { + mEventListener.onChannelScanDone(); } } @@ -165,10 +161,8 @@ public class EventDetector { if (!found) { mVctProgramNumberSet.add(channelProgramNumber); } - if (!mEventListeners.isEmpty()) { - for (EventListener eventListener : mEventListeners) { - eventListener.onChannelDetected(tunerChannel, !found); - } + if (mEventListener != null) { + mEventListener.onChannelDetected(tunerChannel, !found); } } }; @@ -203,9 +197,11 @@ public class EventDetector { /** * Creates a detector for ATSC TV channles and program information. * @param usbTunerInteface {@link TunerHal} + * @param listener for ATSC TV channels and program information */ - public EventDetector(TunerHal usbTunerInteface) { + public EventDetector(TunerHal usbTunerInteface, EventListener listener) { mTunerHal = usbTunerInteface; + mEventListener = listener; } private void reset() { @@ -262,28 +258,4 @@ public class EventDetector { public List<TunerChannel> getMalFormedChannels() { return mTsParser.getMalFormedChannels(); } - - /** - * Registers an EventListener. - * @param eventListener the listener to be registered - */ - public void registerListener(EventListener eventListener) { - if (mTsParser != null) { - // Resets the version numbers so that the new listener can receive the EIT items. - // Otherwise, each EIT session is handled only once unless there is a new version. - mTsParser.resetDataVersions(); - } - mEventListeners.add(eventListener); - } - - /** - * Unregisters an EventListener. - * @param eventListener the listener to be unregistered - */ - public void unregisterListener(EventListener eventListener) { - boolean removed = mEventListeners.remove(eventListener); - if (!removed && DEBUG) { - Log.d(TAG, "Cannot unregister a non-registered listener!"); - } - } } diff --git a/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java b/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java index 46ff4ea1..61de24f4 100644 --- a/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java +++ b/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java @@ -24,8 +24,8 @@ import com.android.tv.tuner.data.PsiData.PatItem; import com.android.tv.tuner.data.PsiData.PmtItem; import com.android.tv.tuner.data.PsipData.EitItem; import com.android.tv.tuner.data.PsipData.VctItem; -import com.android.tv.tuner.data.Track.AtscAudioTrack; -import com.android.tv.tuner.data.Track.AtscCaptionTrack; +import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; +import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.source.FileTsStreamer; import com.android.tv.tuner.ts.TsParser; diff --git a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java index 0be29f25..6ec55e4f 100644 --- a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java +++ b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java @@ -33,17 +33,13 @@ import android.support.annotation.MainThread; import android.support.annotation.Nullable; import android.util.Log; -import android.util.Pair; -import com.google.android.exoplayer.C; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.recording.RecordingCapability; import com.android.tv.dvr.DvrStorageStatusManager; -import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.RecordedProgram; import com.android.tv.tuner.DvbDeviceAccessor; import com.android.tv.tuner.data.PsipData; -import com.android.tv.tuner.data.PsipData.EitItem; -import com.android.tv.tuner.data.Track.AtscCaptionTrack; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.exoplayer.ExoPlayerSampleExtractor; import com.android.tv.tuner.exoplayer.SampleExtractor; @@ -57,10 +53,10 @@ import java.io.File; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Random; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** @@ -75,7 +71,6 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, private static final String SORT_BY_TIME = TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS + ", " + TvContract.Programs.COLUMN_CHANNEL_ID + ", " + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS; - private static final long TUNING_RETRY_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4); private static final long STORAGE_MONITOR_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4); private static final long MIN_PARTIAL_RECORDING_DURATION_MS = TimeUnit.SECONDS.toMillis(10); private static final long PREPARE_RECORDER_POLL_MS = 50; @@ -85,23 +80,20 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, private static final int MSG_STOP_RECORDING = 4; private static final int MSG_MONITOR_STORAGE_STATUS = 5; private static final int MSG_RELEASE = 6; - private static final int MSG_UPDATE_CC_INFO = 7; private final RecordingCapability mCapabilities; public RecordingCapability getCapabilities() { return mCapabilities; } - @IntDef({STATE_IDLE, STATE_TUNING, STATE_TUNED, STATE_RECORDING}) + @IntDef({STATE_IDLE, STATE_TUNED, STATE_RECORDING}) @Retention(RetentionPolicy.SOURCE) public @interface DvrSessionState {} private static final int STATE_IDLE = 1; - private static final int STATE_TUNING = 2; - private static final int STATE_TUNED = 3; - private static final int STATE_RECORDING = 4; + private static final int STATE_TUNED = 2; + private static final int STATE_RECORDING = 3; private static final long CHANNEL_ID_NONE = -1; - private static final int MAX_TUNING_RETRY = 6; private final Context mContext; private final ChannelDataManager mChannelDataManager; @@ -116,16 +108,13 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, private long mRecordStartTime; private long mRecordEndTime; private boolean mRecorderRunning; + private BufferManager mBufferManager; private SampleExtractor mRecorder; private final TunerRecordingSession mSession; @DvrSessionState private int mSessionState = STATE_IDLE; private final String mInputId; private Uri mProgramUri; - private PsipData.EitItem mCurrenProgram; - private List<AtscCaptionTrack> mCaptionTracks; - private DvrStorageManager mDvrStorageManager; - public TunerRecordingSessionWorker(Context context, String inputId, ChannelDataManager dataManager, TunerRecordingSession session) { mRandom.setSeed(System.nanoTime()); @@ -168,7 +157,6 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, if (mChannel == null || mChannel.compareTo(channel) != 0) { return; } - mHandler.obtainMessage(MSG_UPDATE_CC_INFO, new Pair<>(channel, items)).sendToTarget(); mChannelDataManager.notifyEventDetected(channel, items); } @@ -190,7 +178,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, @MainThread public void tune(Uri channelUri) { mHandler.removeCallbacksAndMessages(null); - mHandler.obtainMessage(MSG_TUNE, 0, 0, channelUri).sendToTarget(); + mHandler.obtainMessage(MSG_TUNE, channelUri).sendToTarget(); } /** @@ -223,22 +211,11 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, switch (msg.what) { case MSG_TUNE: { Uri channelUri = (Uri) msg.obj; - int retryCount = msg.arg1; if (DEBUG) Log.d(TAG, "Tune to " + channelUri); if (doTune(channelUri)) { - if (mSessionState == STATE_TUNED) { - mSession.onTuned(channelUri); - } else { - Log.w(TAG, "Tuner stream cannot be created due to resource shortage."); - if (retryCount < MAX_TUNING_RETRY) { - Message tuneMsg = - mHandler.obtainMessage(MSG_TUNE, retryCount + 1, 0, channelUri); - mHandler.sendMessageDelayed(tuneMsg, TUNING_RETRY_INTERVAL_MS); - } else { - mSession.onError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY); - reset(); - } - } + mSession.onTuned(channelUri); + } else { + reset(); } return true; } @@ -304,12 +281,6 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, mHandler.getLooper().quitSafely(); return true; } - case MSG_UPDATE_CC_INFO: { - Pair<TunerChannel, List<EitItem>> pair = - (Pair<TunerChannel, List<EitItem>>) msg.obj; - updateCaptionTracks(pair.first, pair.second); - return true; - } } return false; } @@ -339,17 +310,20 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, mRecorder.release(); mRecorder = null; } + if (mBufferManager != null) { + mBufferManager.close(); + mBufferManager = null; + } if (mTunerSource != null) { mSourceManager.releaseDataSource(mTunerSource); mTunerSource = null; } - mDvrStorageManager = null; mSessionState = STATE_IDLE; mRecorderRunning = false; } private boolean doTune(Uri channelUri) { - if (mSessionState != STATE_IDLE && mSessionState != STATE_TUNING) { + if (mSessionState != STATE_IDLE) { mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); Log.e(TAG, "Tuning was requested from wrong status."); return false; @@ -359,10 +333,6 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); Log.w(TAG, "Failed to start recording. Couldn't find the channel for " + mChannel); return false; - } else if (mChannel.isRecordingProhibited()) { - mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); - Log.w(TAG, "Failed to start recording. Not a recordable channel: " + mChannel); - return false; } if (!mDvrStorageStatusManager.isStorageSufficient()) { mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); @@ -371,9 +341,9 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, } mTunerSource = mSourceManager.createDataSource(mContext, mChannel, this); if (mTunerSource == null) { - // Retry tuning in this case. - mSessionState = STATE_TUNING; - return true; + mSession.onError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY); + Log.w(TAG, "Tuner stream cannot be created due to resource shortage."); + return false; } mSessionState = STATE_TUNED; return true; @@ -395,10 +365,10 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, } // Since tuning might be happened a while ago, shifts the start position of tuned source. mTunerSource.shiftStartPosition(mTunerSource.getBufferedPosition()); + mBufferManager = new BufferManager(new DvrStorageManager(mStorageDir, true)); mRecordStartTime = System.currentTimeMillis(); - mDvrStorageManager = new DvrStorageManager(mStorageDir, true); - mRecorder = new ExoPlayerSampleExtractor(Uri.EMPTY, mTunerSource, - new BufferManager(mDvrStorageManager), this, true); + mRecorder = new ExoPlayerSampleExtractor(Uri.EMPTY, mTunerSource, mBufferManager, this, + true); mRecorder.setOnCompletionListener(this, mHandler); mProgramUri = programUri; mSessionState = STATE_RECORDING; @@ -422,34 +392,6 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, Log.i(TAG, "Recording stopped"); } - private void updateCaptionTracks(TunerChannel channel, List<PsipData.EitItem> items) { - if (mChannel == null || channel == null || mChannel.compareTo(channel) != 0 - || items == null || items.isEmpty()) { - return; - } - PsipData.EitItem currentProgram = getCurrentProgram(items); - if (currentProgram == null || !currentProgram.hasCaptionTrack() - || mCurrenProgram != null && mCurrenProgram.compareTo(currentProgram) == 0) { - return; - } - mCurrenProgram = currentProgram; - mCaptionTracks = new ArrayList<>(currentProgram.getCaptionTracks()); - if (DEBUG) { - Log.d(TAG, "updated " + mCaptionTracks.size() + " caption tracks for " - + currentProgram); - } - } - - private PsipData.EitItem getCurrentProgram(List<PsipData.EitItem> items) { - for (PsipData.EitItem item : items) { - if (mRecordStartTime >= item.getStartTimeUtcMillis() - && mRecordStartTime < item.getEndTimeUtcMillis()) { - return item; - } - } - return null; - } - private static class Program { private final long mChannelId; private final String mTitle; @@ -624,25 +566,15 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, return; } Log.i(TAG, "recording finished " + (success ? "completely" : "partially")); - long recordEndTime = - (lastExtractedPositionUs == C.UNKNOWN_TIME_US) - ? System.currentTimeMillis() - : mRecordStartTime + lastExtractedPositionUs / 1000; - Uri uri = - insertRecordedProgram( - getRecordedProgram(), - mChannel.getChannelId(), - Uri.fromFile(mStorageDir).toString(), - 1024 * 1024, - mRecordStartTime, - recordEndTime); + Uri uri = insertRecordedProgram(getRecordedProgram(), mChannel.getChannelId(), + Uri.fromFile(mStorageDir).toString(), 1024 * 1024, mRecordStartTime, + mRecordStartTime + TimeUnit.MICROSECONDS.toMillis(lastExtractedPositionUs)); if (uri == null) { new DeleteRecordingTask().execute(mStorageDir); mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); Log.e(TAG, "Inserting a recording to DB failed"); return; } - mDvrStorageManager.writeCaptionInfoFiles(mCaptionTracks); mSession.onRecordFinished(uri); } diff --git a/src/com/android/tv/tuner/tvinput/TunerSession.java b/src/com/android/tv/tuner/tvinput/TunerSession.java index d1ee3c6f..5c61402e 100644 --- a/src/com/android/tv/tuner/tvinput/TunerSession.java +++ b/src/com/android/tv/tuner/tvinput/TunerSession.java @@ -41,7 +41,7 @@ import com.android.tv.tuner.R; import com.android.tv.tuner.cc.CaptionLayout; import com.android.tv.tuner.cc.CaptionTrackRenderer; import com.android.tv.tuner.data.Cea708Data.CaptionEvent; -import com.android.tv.tuner.data.Track.AtscCaptionTrack; +import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; import com.android.tv.tuner.exoplayer.buffer.BufferManager; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.util.GlobalSettingsUtils; @@ -81,7 +81,8 @@ public class TunerSession extends TvInputService.Session implements Handler.Call private boolean mPlayPaused; private long mTuneStartTimestamp; - public TunerSession(Context context, ChannelDataManager channelDataManager) { + public TunerSession(Context context, ChannelDataManager channelDataManager, + BufferManager bufferManager) { super(context); mContext = context; mUiHandler = new Handler(this); @@ -96,9 +97,12 @@ public class TunerSession extends TvInputService.Session implements Handler.Call mStatusView.setVisibility(showDebug ? View.VISIBLE : View.INVISIBLE); mAudioStatusView = (TextView) mOverlayView.findViewById(R.id.audio_status); mAudioStatusView.setVisibility(View.INVISIBLE); + mAudioStatusView.setText(Html.fromHtml(StatusTextUtils.getAudioWarningInHTML( + context.getString(R.string.ut_surround_sound_disabled)))); CaptionLayout captionLayout = (CaptionLayout) mOverlayView.findViewById(R.id.caption); mCaptionTrackRenderer = new CaptionTrackRenderer(captionLayout); - mSessionWorker = new TunerSessionWorker(context, channelDataManager, this); + mSessionWorker = new TunerSessionWorker(context, channelDataManager, + bufferManager, this); } public boolean isReleased() { @@ -268,13 +272,10 @@ public class TunerSession extends TvInputService.Session implements Handler.Call // setting is "never". final int value = GlobalSettingsUtils.getEncodedSurroundOutputSettings(mContext); if (value == GlobalSettingsUtils.ENCODED_SURROUND_OUTPUT_NEVER) { - mAudioStatusView.setText(Html.fromHtml(StatusTextUtils.getAudioWarningInHTML( - mContext.getString(R.string.ut_surround_sound_disabled)))); + mAudioStatusView.setVisibility(View.VISIBLE); } else { - mAudioStatusView.setText(Html.fromHtml(StatusTextUtils.getAudioWarningInHTML( - mContext.getString(R.string.audio_passthrough_not_supported)))); + Log.e(TAG, "Audio is unavailable, surround sound setting is " + value); } - mAudioStatusView.setVisibility(View.VISIBLE); return true; } case MSG_UI_HIDE_AUDIO_UNPLAYABLE: { diff --git a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java index 41f8ce5f..5230298e 100644 --- a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java +++ b/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java @@ -35,7 +35,6 @@ import android.support.annotation.AnyThread; import android.support.annotation.MainThread; import android.support.annotation.WorkerThread; import android.text.Html; -import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.util.SparseArray; @@ -48,30 +47,28 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.common.TvContentRatingCache; import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.data.Cea708Data; -import com.android.tv.tuner.data.Channel; import com.android.tv.tuner.data.PsipData.EitItem; import com.android.tv.tuner.data.PsipData.TvTracksInterface; -import com.android.tv.tuner.data.Track.AtscAudioTrack; -import com.android.tv.tuner.data.Track.AtscCaptionTrack; import com.android.tv.tuner.data.TunerChannel; +import com.android.tv.tuner.data.nano.Channel; +import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; +import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder; import com.android.tv.tuner.exoplayer.buffer.BufferManager; -import com.android.tv.tuner.exoplayer.buffer.BufferManager.StorageManager; import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager; import com.android.tv.tuner.exoplayer.MpegTsPlayer; -import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager; import com.android.tv.tuner.source.TsDataSource; import com.android.tv.tuner.source.TsDataSourceManager; import com.android.tv.tuner.util.StatusTextUtils; -import com.android.tv.tuner.util.SystemPropertiesProxy; import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Objects; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.CountDownLatch; /** * {@link TunerSessionWorker} implements a handler thread which processes TV input jobs @@ -85,9 +82,6 @@ public class TunerSessionWorker implements PlaybackBufferListener, private static final boolean DEBUG = false; private static final boolean ENABLE_PROFILER = true; private static final String PLAY_FROM_CHANNEL = "channel"; - private static final String MAX_BUFFER_SIZE_KEY = "tv.tuner.buffersize_mbytes"; - private static final int MAX_BUFFER_SIZE_DEF = 2 * 1024; // 2GB - private static final int MIN_BUFFER_SIZE_DEF = 256; // 256MB // Public messages public static final int MSG_SELECT_TRACK = 1; @@ -153,18 +147,10 @@ public class TunerSessionWorker implements PlaybackBufferListener, private static final int EXPECTED_KEY_FRAME_INTERVAL_MS = 500; private static final int MIN_TRICKPLAY_SEEK_INTERVAL_MS = 20; private static final int TRICKPLAY_MONITOR_INTERVAL_MS = 250; - private static final int RELEASE_WAIT_INTERVAL_MS = 50; - - // Since release() is done asynchronously, synchronization between multiple TunerSessionWorker - // creation/release is required. - // This is used to guarantee that at most one active TunerSessionWorker exists at any give time. - private static Semaphore sActiveSessionSemaphore = new Semaphore(1); private final Context mContext; private final ChannelDataManager mChannelDataManager; private final TsDataSourceManager mSourceManager; - private final int mMaxTrickplayBufferSizeMb; - private final File mTrickplayBufferDir; private volatile Surface mSurface; private volatile float mVolume = 1.0f; private volatile boolean mCaptionEnabled; @@ -173,7 +159,6 @@ public class TunerSessionWorker implements PlaybackBufferListener, private volatile Long mRecordingDuration; private volatile long mRecordStartTimeMs; private volatile long mBufferStartTimeMs; - private volatile boolean mTrickplayDisabled; private String mRecordingId; private final Handler mHandler; private int mRetryCount; @@ -192,19 +177,19 @@ public class TunerSessionWorker implements PlaybackBufferListener, private TvContentRating mUnblockedContentRating; private long mLastPositionMs; private AudioCapabilities mAudioCapabilities; + private final CountDownLatch mReleaseLatch = new CountDownLatch(1); private long mLastLimitInBytes; + private long mLastPositionInBytes; + private final BufferManager mBufferManager; private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance(); private final TunerSession mSession; private int mPlayerState = ExoPlayer.STATE_IDLE; private long mPreparingStartTimeMs; private long mBufferingStartTimeMs; private long mReadyStartTimeMs; - private boolean mIsActiveSession; - private boolean mReleaseRequested; // Guarded by mReleaseLock - private final Object mReleaseLock = new Object(); public TunerSessionWorker(Context context, ChannelDataManager channelDataManager, - TunerSession tunerSession) { + BufferManager bufferManager, TunerSession tunerSession) { if (DEBUG) Log.d(TAG, "TunerSessionWorker created"); mContext = context; @@ -226,10 +211,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); mCaptionEnabled = captioningManager.isEnabled(); mPlaybackParams.setSpeed(1.0f); - mMaxTrickplayBufferSizeMb = - SystemPropertiesProxy.getInt(MAX_BUFFER_SIZE_KEY, MAX_BUFFER_SIZE_DEF); - mTrickplayBufferDir = context.getCacheDir(); - mTrickplayDisabled = mTrickplayBufferDir == null; + mBufferManager = bufferManager; mPreparingStartTimeMs = INVALID_TIME; mBufferingStartTimeMs = INVALID_TIME; mReadyStartTimeMs = INVALID_TIME; @@ -303,21 +285,24 @@ public class TunerSessionWorker implements PlaybackBufferListener, } private Long getDurationForRecording(String recordingId) { - DvrStorageManager storageManager = + try { + DvrStorageManager storageManager = new DvrStorageManager(new File(getRecordingPath()), false); - List<BufferManager.TrackFormat> trackFormatList = - storageManager.readTrackInfoFiles(false); - if (trackFormatList.isEmpty()) { - trackFormatList = storageManager.readTrackInfoFiles(true); - } - if (!trackFormatList.isEmpty()) { - BufferManager.TrackFormat trackFormat = trackFormatList.get(0); - Long durationUs = trackFormat.format.getLong(MediaFormat.KEY_DURATION); + Pair<String, MediaFormat> trackInfo = null; + try { + trackInfo = storageManager.readTrackInfoFile(false); + } catch (FileNotFoundException e) { + } + if (trackInfo == null) { + trackInfo = storageManager.readTrackInfoFile(true); + } + Long durationUs = trackInfo.second.getLong(MediaFormat.KEY_DURATION); // we need duration by milli for trickplay notification. return durationUs != null ? durationUs / 1000 : null; + } catch (IOException e) { + Log.e(TAG, "meta file for recording was not found: " + recordingId); + return null; } - Log.e(TAG, "meta file for recording was not found: " + recordingId); - return null; } @MainThread @@ -356,12 +341,16 @@ public class TunerSessionWorker implements PlaybackBufferListener, @MainThread public void release() { if (DEBUG) Log.d(TAG, "release()"); - synchronized (mReleaseLock) { - mReleaseRequested = true; - } mChannelDataManager.setListener(null); mHandler.removeCallbacksAndMessages(null); mHandler.sendEmptyMessage(MSG_RELEASE); + try { + mReleaseLatch.await(); + } catch (InterruptedException e) { + Log.e(TAG, "Couldn't wait for finish of MSG_RELEASE", e); + } finally { + mHandler.getLooper().quitSafely(); + } } // MpegTsPlayer.Listener @@ -378,7 +367,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (playbackState == ExoPlayer.STATE_READY) { if (DEBUG) Log.d(TAG, "ExoPlayer ready"); if (!mPlayerStarted) { - sendMessage(MSG_START_PLAYBACK, System.identityHashCode(mPlayer)); + sendMessage(MSG_START_PLAYBACK, mPlayer); } mReadyStartTimeMs = SystemClock.elapsedRealtime(); } else if (playbackState == ExoPlayer.STATE_PREPARING) { @@ -390,7 +379,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, // notification of STATE_ENDED from MpegTsPlayer will be ignored afterwards. Log.i(TAG, "Player ended: end of stream"); if (mChannel != null) { - sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)); + sendMessage(MSG_RETRY_PLAYBACK, mPlayer); } } mPlayerState = playbackState; @@ -408,8 +397,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, // If we are playing live stream, retrying playback maybe helpful. But for recorded stream, // retrying playback is not helpful. if (mChannel != null) { - mHandler.obtainMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)) - .sendToTarget(); + mHandler.obtainMessage(MSG_RETRY_PLAYBACK, mPlayer).sendToTarget(); } } @@ -427,12 +415,8 @@ public class TunerSessionWorker implements PlaybackBufferListener, public void onDrawnToSurface(MpegTsPlayer player, Surface surface) { if (mSurface != null && mPlayerStarted) { if (DEBUG) Log.d(TAG, "MSG_DRAWN_TO_SURFACE"); - if (mRecordingId != null) { - // Workaround of b/33298048: set it to 1 instead of 0. - mBufferStartTimeMs = mRecordStartTimeMs = 1; - } else { - mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis(); - } + mBufferStartTimeMs = mRecordStartTimeMs = + (mRecordingId != null) ? 0 : System.currentTimeMillis(); notifyVideoAvailable(); mReportedDrawnToSurface = true; @@ -515,8 +499,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, @Override public void onDiskTooSlow() { - mTrickplayDisabled = true; - sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)); + sendMessage(MSG_RETRY_PLAYBACK, mPlayer); } // EventDetector.EventListener @@ -619,28 +602,6 @@ public class TunerSessionWorker implements PlaybackBufferListener, return true; } notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); - if (!mIsActiveSession) { - // Wait until release is finished if there is a pending release. - try { - while (!sActiveSessionSemaphore.tryAcquire( - RELEASE_WAIT_INTERVAL_MS, TimeUnit.MILLISECONDS)) { - synchronized (mReleaseLock) { - if (mReleaseRequested) { - return true; - } - } - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - synchronized (mReleaseLock) { - if (mReleaseRequested) { - sActiveSessionSemaphore.release(); - return true; - } - } - mIsActiveSession = true; - } Uri channelUri = (Uri) msg.obj; String recording = null; long channelId = parseChannel(channelUri); @@ -655,8 +616,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); return true; } - clearCallbacksAndMessagesSafely(); - mChannelDataManager.removeAllCallbacksAndMessages(); + mHandler.removeCallbacksAndMessages(null); if (channel != null) { mChannelDataManager.requestProgramsData(channel); } @@ -664,8 +624,8 @@ public class TunerSessionWorker implements PlaybackBufferListener, // TODO: Need to refactor. notifyContentAllowed() should not be called if parental // control is turned on. mSession.notifyContentAllowed(); - resetTvTracks(); resetPlayback(); + resetTvTracks(); mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS); return true; @@ -673,7 +633,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, case MSG_STOP_TUNE: { if (DEBUG) Log.d(TAG, "MSG_STOP_TUNE"); mChannel = null; - stopPlayback(true); + stopPlayback(); stopCaptionTrack(); resetTvTracks(); notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); @@ -682,17 +642,14 @@ public class TunerSessionWorker implements PlaybackBufferListener, case MSG_RELEASE: { if (DEBUG) Log.d(TAG, "MSG_RELEASE"); mHandler.removeCallbacksAndMessages(null); - stopPlayback(true); + stopPlayback(); stopCaptionTrack(); mSourceManager.release(); - mHandler.getLooper().quitSafely(); - if (mIsActiveSession) { - sActiveSessionSemaphore.release(); - } + mReleaseLatch.countDown(); return true; } case MSG_RETRY_PLAYBACK: { - if (System.identityHashCode(mPlayer) == (int) msg.obj) { + if (mPlayer == msg.obj) { Log.i(TAG, "Retrying the playback for channel: " + mChannel); mHandler.removeMessages(MSG_RETRY_PLAYBACK); // When there is a request of retrying playback, don't reuse TunerHal. @@ -701,14 +658,13 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (DEBUG) { Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount); } - mChannelDataManager.removeAllCallbacksAndMessages(); if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) { resetPlayback(); } else { // When it reaches this point, it may be due to an error that occurred in // the tuner device. Calling stopPlayback() resets the tuner device // to recover from the error. - stopPlayback(false); + stopPlayback(); stopCaptionTrack(); notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); @@ -723,14 +679,13 @@ public class TunerSessionWorker implements PlaybackBufferListener, } case MSG_RESET_PLAYBACK: { if (DEBUG) Log.d(TAG, "MSG_RESET_PLAYBACK"); - mChannelDataManager.removeAllCallbacksAndMessages(); resetPlayback(); return true; } case MSG_START_PLAYBACK: { if (DEBUG) Log.d(TAG, "MSG_START_PLAYBACK"); if (mChannel != null || mRecordingId != null) { - startPlayback((int) msg.obj); + startPlayback(msg.obj); } return true; } @@ -835,11 +790,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, return true; } case MSG_RESCHEDULE_PROGRAMS: { - if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) { - mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS); - } else { - doReschedulePrograms(); - } + doReschedulePrograms(); return true; } case MSG_PARENTAL_CONTROLS: { @@ -863,8 +814,11 @@ public class TunerSessionWorker implements PlaybackBufferListener, return true; } case MSG_SELECT_TRACK: { - if (mChannel != null || mRecordingId != null) { + if (mChannel != null) { doSelectTrack(msg.arg1, (String) msg.obj); + } else if (mRecordingId != null) { + // TODO : mChannel == null && mRecordingId != null + Log.d(TAG, "track selected for recording"); } return true; } @@ -955,6 +909,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, } TsDataSource source = mPlayer.getDataSource(); long limitInBytes = source != null ? source.getBufferedPosition() : 0L; + long positionInBytes = source != null ? source.getLastReadPosition() : 0L; if (TunerDebug.ENABLED) { TunerDebug.calculateDiff(); mSession.sendUiMessage(TunerSession.MSG_UI_SET_STATUS_TEXT, @@ -972,8 +927,14 @@ public class TunerSessionWorker implements PlaybackBufferListener, TunerDebug.getVideoPtsUsRate() ))); } + if (DEBUG) { + Log.d(TAG, String.format("MSG_CHECK_SIGNAL position: %d, limit: %d", + positionInBytes, limitInBytes)); + } mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE); long currentTime = SystemClock.elapsedRealtime(); + boolean noBufferRead = positionInBytes == mLastPositionInBytes + && limitInBytes == mLastLimitInBytes; boolean isBufferingTooLong = mBufferingStartTimeMs != INVALID_TIME && currentTime - mBufferingStartTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; @@ -982,11 +943,11 @@ public class TunerSessionWorker implements PlaybackBufferListener, > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; boolean isWeakSignal = source != null && mChannel.getType() == Channel.TYPE_TUNER - && (isBufferingTooLong || isPreparingTooLong); + && (noBufferRead || isBufferingTooLong || isPreparingTooLong); if (isWeakSignal && !mReportedWeakSignal) { if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) { - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK, - System.identityHashCode(mPlayer)), PLAYBACK_RETRY_DELAY_MS); + mHandler.sendMessageDelayed(mHandler.obtainMessage( + MSG_RETRY_PLAYBACK, mPlayer), PLAYBACK_RETRY_DELAY_MS); } if (mPlayer != null) { mPlayer.setAudioTrack(false); @@ -1005,6 +966,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, } } mLastLimitInBytes = limitInBytes; + mLastPositionInBytes = positionInBytes; mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS); return true; } @@ -1037,8 +999,15 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (trackId == null) { return; } - if (numTrackId != mPlayer.getSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO)) { - mPlayer.setSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO, numTrackId); + AtscAudioTrack audioTrack = mAudioTrackMap.get(numTrackId); + if (audioTrack == null) { + return; + } + int oldAudioPid = mChannel.getAudioPid(); + mChannel.selectAudioTrack(audioTrack.index); + int newAudioPid = mChannel.getAudioPid(); + if (oldAudioPid != newAudioPid) { + mPlayer.setSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO, audioTrack.index); } mSession.notifyTrackSelected(type, trackId); } else if (type == TvTrackInfo.TYPE_SUBTITLE) { @@ -1061,22 +1030,11 @@ public class TunerSessionWorker implements PlaybackBufferListener, } } - private MpegTsPlayer createPlayer(AudioCapabilities capabilities) { + private MpegTsPlayer createPlayer(AudioCapabilities capabilities, BufferManager bufferManager) { if (capabilities == null) { Log.w(TAG, "No Audio Capabilities"); } - BufferManager bufferManager = null; - if (mRecordingId != null) { - StorageManager storageManager = - new DvrStorageManager(new File(getRecordingPath()), false); - bufferManager = new BufferManager(storageManager); - updateCaptionTracks(((DvrStorageManager)storageManager).readCaptionInfoFiles()); - } else if (!mTrickplayDisabled && mMaxTrickplayBufferSizeMb >= MIN_BUFFER_SIZE_DEF) { - bufferManager = new BufferManager(new TrickplayStorageManager(mContext, - mTrickplayBufferDir, 1024L * 1024 * mMaxTrickplayBufferSizeMb)); - } else { - Log.w(TAG, "Trickplay is disabled."); - } + MpegTsPlayer player = new MpegTsPlayer( new MpegTsRendererBuilder(mContext, bufferManager, this), mHandler, mSourceManager, capabilities, this); @@ -1111,26 +1069,24 @@ public class TunerSessionWorker implements PlaybackBufferListener, } private void updateTvTracks(TvTracksInterface tvTracksInterface, boolean fromPmt) { - synchronized (tvTracksInterface) { - if (DEBUG) { - Log.d(TAG, "UpdateTvTracks " + tvTracksInterface); - } - List<AtscAudioTrack> audioTracks = tvTracksInterface.getAudioTracks(); - List<AtscCaptionTrack> captionTracks = tvTracksInterface.getCaptionTracks(); - // According to ATSC A/69 chapter 6.9, both PMT and EIT should have descriptors for audio - // tracks, but in real world, we see some bogus audio track info in EIT, so, we trust audio - // track info in PMT more and use info in EIT only when we have nothing. - if (audioTracks != null && !audioTracks.isEmpty() - && (mChannel == null || mChannel.getAudioTracks() == null || fromPmt)) { - updateAudioTracks(audioTracks); - } - if (captionTracks == null || captionTracks.isEmpty()) { - if (tvTracksInterface.hasCaptionTrack()) { - updateCaptionTracks(captionTracks); - } - } else { + if (DEBUG) { + Log.d(TAG, "UpdateTvTracks " + tvTracksInterface); + } + List<AtscAudioTrack> audioTracks = tvTracksInterface.getAudioTracks(); + List<AtscCaptionTrack> captionTracks = tvTracksInterface.getCaptionTracks(); + // According to ATSC A/69 chapter 6.9, both PMT and EIT should have descriptors for audio + // tracks, but in real world, we see some bogus audio track info in EIT, so, we trust audio + // track info in PMT more and use info in EIT only when we have nothing. + if (audioTracks != null && !audioTracks.isEmpty() + && (mChannel.getAudioTracks() == null || fromPmt)) { + updateAudioTracks(audioTracks); + } + if (captionTracks == null || captionTracks.isEmpty()) { + if (tvTracksInterface.hasCaptionTrack()) { updateCaptionTracks(captionTracks); } + } else { + updateCaptionTracks(captionTracks); } } @@ -1176,24 +1132,25 @@ public class TunerSessionWorker implements PlaybackBufferListener, int audioTrackCount = mPlayer.getTrackCount(MpegTsPlayer.TRACK_TYPE_AUDIO); removeTvTracks(TvTrackInfo.TYPE_AUDIO); for (int i = 0; i < audioTrackCount; i++) { - // We use language information from EIT/VCT only when the player does not provide - // languages. - com.google.android.exoplayer.MediaFormat infoFromPlayer = - mPlayer.getTrackFormat(MpegTsPlayer.TRACK_TYPE_AUDIO, i); - AtscAudioTrack infoFromEit = mAudioTrackMap.get(i); - AtscAudioTrack infoFromVct = (mChannel != null - && mChannel.getAudioTracks().size() == mAudioTrackMap.size() - && i < mChannel.getAudioTracks().size()) - ? mChannel.getAudioTracks().get(i) : null; - String language = !TextUtils.isEmpty(infoFromPlayer.language) ? infoFromPlayer.language - : (infoFromEit != null && infoFromEit.language != null) ? infoFromEit.language - : (infoFromVct != null && infoFromVct.language != null) - ? infoFromVct.language : null; + AtscAudioTrack audioTrack = mAudioTrackMap.get(i); + if (audioTrack == null) { + continue; + } + String language = audioTrack.language; + if (language == null && mChannel.getAudioTracks() != null + && mChannel.getAudioTracks().size() == mAudioTrackMap.size()) { + // If a language is not present, use a language field in PMT section parsed. + language = mChannel.getAudioTracks().get(i).language; + } + // Save the index to the audio track. + // Later, when an audio track is selected, both the audio pid and its audio stream + // type reside in the selected index position of the tuner channel's audio data. + audioTrack.index = i; TvTrackInfo.Builder builder = new TvTrackInfo.Builder( TvTrackInfo.TYPE_AUDIO, AUDIO_TRACK_PREFIX + i); builder.setLanguage(language); - builder.setAudioChannelCount(infoFromPlayer.channelCount); - builder.setAudioSampleRate(infoFromPlayer.sampleRate); + builder.setAudioChannelCount(audioTrack.channelCount); + builder.setAudioSampleRate(audioTrack.sampleRate); TvTrackInfo track = builder.build(); mTvTracks.add(track); } @@ -1269,10 +1226,8 @@ public class TunerSessionWorker implements PlaybackBufferListener, } } - private void stopPlayback(boolean removeChannelDataCallbacks) { - if (removeChannelDataCallbacks) { - mChannelDataManager.removeAllCallbacksAndMessages(); - } + private void stopPlayback() { + mChannelDataManager.removeAllCallbacksAndMessages(); if (mPlayer != null) { mPlayer.setPlayWhenReady(false); mPlayer.release(); @@ -1289,9 +1244,9 @@ public class TunerSessionWorker implements PlaybackBufferListener, } } - private void startPlayback(int playerHashCode) { + private void startPlayback(Object playerObj) { // TODO: provide hasAudio()/hasVideo() for play recordings. - if (mPlayer == null || System.identityHashCode(mPlayer) != playerHashCode) { + if (mPlayer == null || mPlayer != playerObj) { return; } if (mChannel != null && !mChannel.hasAudio()) { @@ -1304,7 +1259,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (mChannel != null && ((mChannel.hasAudio() && !mPlayer.hasAudio()) || (mChannel.hasVideo() && !mPlayer.hasVideo()))) { // Tracks haven't been detected in the extractor. Try again. - sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)); + sendMessage(MSG_RETRY_PLAYBACK, mPlayer); return; } // Since mSurface is volatile, we define a local variable surface to keep the same value @@ -1331,7 +1286,9 @@ public class TunerSessionWorker implements PlaybackBufferListener, return; } mSourceManager.setKeepTuneStatus(true); - MpegTsPlayer player = createPlayer(mAudioCapabilities); + BufferManager bufferManager = mChannel != null ? mBufferManager : new BufferManager( + new DvrStorageManager(new File(getRecordingPath()), false)); + MpegTsPlayer player = createPlayer(mAudioCapabilities, bufferManager); player.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER); player.setVideoEventListener(this); player.setCaptionServiceNumber(mCaptionTrack != null ? @@ -1343,8 +1300,8 @@ public class TunerSessionWorker implements PlaybackBufferListener, // When prepare failed, there may be some errors related to hardware. In that // case, retry playback immediately may not help. notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK, - System.identityHashCode(mPlayer)), PLAYBACK_RETRY_DELAY_MS); + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK, mPlayer), + PLAYBACK_RETRY_DELAY_MS); } } else { mPlayer = player; @@ -1357,7 +1314,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, private void resetPlayback() { long timestamp, oldTimestamp; timestamp = SystemClock.elapsedRealtime(); - stopPlayback(false); + stopPlayback(); stopCaptionTrack(); if (ENABLE_PROFILER) { oldTimestamp = timestamp; @@ -1379,12 +1336,8 @@ public class TunerSessionWorker implements PlaybackBufferListener, mRecordingDuration = recording != null ? getDurationForRecording(recording) : null; mProgram = null; mPrograms = null; - if (mRecordingId != null) { - // Workaround of b/33298048: set it to 1 instead of 0. - mBufferStartTimeMs = mRecordStartTimeMs = 1; - } else { - mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis(); - } + mBufferStartTimeMs = mRecordStartTimeMs = + (mRecordingId != null) ? 0 : System.currentTimeMillis(); mLastPositionMs = 0; mCaptionTrack = null; mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS); @@ -1591,15 +1544,15 @@ public class TunerSessionWorker implements PlaybackBufferListener, } mChannelBlocked = channelBlocked; if (mChannelBlocked) { - clearCallbacksAndMessagesSafely(); - stopPlayback(true); + mHandler.removeCallbacksAndMessages(null); + stopPlayback(); resetTvTracks(); if (contentRating != null) { mSession.notifyContentBlocked(contentRating); } mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS); } else { - clearCallbacksAndMessagesSafely(); + mHandler.removeCallbacksAndMessages(null); resetPlayback(); mSession.notifyContentAllowed(); mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS, @@ -1609,17 +1562,6 @@ public class TunerSessionWorker implements PlaybackBufferListener, } } - @WorkerThread - private void clearCallbacksAndMessagesSafely() { - // If MSG_RELEASE is removed, TunerSessionWorker will hang forever. - // Do not remove messages, after release is requested from MainThread. - synchronized (mReleaseLock) { - if (!mReleaseRequested) { - mHandler.removeCallbacksAndMessages(null); - } - } - } - private boolean hasEnoughBackwardBuffer() { return mPlayer.getCurrentPosition() + BUFFER_UNDERFLOW_BUFFER_MS >= mBufferStartTimeMs - mRecordStartTimeMs; diff --git a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java index 6594e089..684ebdbd 100644 --- a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java +++ b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java @@ -28,6 +28,9 @@ import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; import com.android.tv.TvApplication; import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.tuner.exoplayer.buffer.BufferManager; +import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager; +import com.android.tv.tuner.util.SystemPropertiesProxy; import java.util.Collections; import java.util.Set; @@ -42,6 +45,9 @@ public class TunerTvInputService extends TvInputService private static final String TAG = "TunerTvInputService"; private static final boolean DEBUG = false; + private static final String MAX_BUFFER_SIZE_KEY = "tv.tuner.buffersize_mbytes"; + private static final int MAX_BUFFER_SIZE_DEF = 2 * 1024; // 2GB + private static final int MIN_BUFFER_SIZE_DEF = 256; // 256MB private static final int DVR_STORAGE_CLEANUP_JOB_ID = 100; // WeakContainer for {@link TvInputSessionImpl} @@ -49,6 +55,7 @@ public class TunerTvInputService extends TvInputService private ChannelDataManager mChannelDataManager; private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver; private AudioCapabilities mAudioCapabilities; + private BufferManager mBufferManager; @Override public void onCreate() { @@ -58,6 +65,7 @@ public class TunerTvInputService extends TvInputService mChannelDataManager = new ChannelDataManager(getApplicationContext()); mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), this); mAudioCapabilitiesReceiver.register(); + mBufferManager = createBufferManager(); if (CommonFeatures.DVR.isEnabled(this)) { JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE); @@ -71,6 +79,11 @@ public class TunerTvInputService extends TvInputService jobScheduler.schedule(job); } } + if (mBufferManager == null) { + Log.i(TAG, "Trickplay is disabled"); + } else { + Log.i(TAG, "Trickplay is enabled"); + } } @Override @@ -79,6 +92,9 @@ public class TunerTvInputService extends TvInputService super.onDestroy(); mChannelDataManager.release(); mAudioCapabilitiesReceiver.unregister(); + if (mBufferManager != null) { + mBufferManager.close(); + } } @Override @@ -90,7 +106,8 @@ public class TunerTvInputService extends TvInputService public Session onCreateSession(String inputId) { if (DEBUG) Log.d(TAG, "onCreateSession"); try { - final TunerSession session = new TunerSession(this, mChannelDataManager); + final TunerSession session = new TunerSession( + this, mChannelDataManager, mBufferManager); mTunerSessions.add(session); session.setAudioCapabilities(mAudioCapabilities); session.setOverlayViewEnabled(true); @@ -112,6 +129,17 @@ public class TunerTvInputService extends TvInputService } } + private BufferManager createBufferManager() { + int maxBufferSizeMb = + SystemPropertiesProxy.getInt(MAX_BUFFER_SIZE_KEY, MAX_BUFFER_SIZE_DEF); + if (maxBufferSizeMb >= MIN_BUFFER_SIZE_DEF) { + return new BufferManager( + new TrickplayStorageManager(getApplicationContext(), getCacheDir(), + 1024L * 1024 * maxBufferSizeMb)); + } + return null; + } + public static String getInputId(Context context) { return TvContract.buildInputId(new ComponentName(context, TunerTvInputService.class)); } diff --git a/src/com/android/tv/tuner/util/PostalCodeUtils.java b/src/com/android/tv/tuner/util/PostalCodeUtils.java deleted file mode 100644 index 3942ce95..00000000 --- a/src/com/android/tv/tuner/util/PostalCodeUtils.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.util; - -import android.content.Context; -import android.location.Address; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.util.Log; - -import com.android.tv.tuner.TunerPreferences; -import com.android.tv.util.LocationUtils; - -import java.io.IOException; -import java.util.Locale; - -/** - * A utility class to update, get, and set the last known postal or zip code. - */ -public class PostalCodeUtils { - private static final String TAG = "PostalCodeUtils"; - private static final String SUPPORTED_COUNTRY_CODE = Locale.US.getCountry(); - - /** Returns {@code true} if postal code has been changed */ - public static boolean updatePostalCode(Context context) - throws IOException, SecurityException, NoPostalCodeException { - String postalCode = getPostalCode(context); - String lastPostalCode = getLastPostalCode(context); - if (TextUtils.isEmpty(postalCode)) { - if (TextUtils.isEmpty(lastPostalCode)) { - throw new NoPostalCodeException(); - } - } else if (!TextUtils.equals(postalCode, lastPostalCode)) { - setLastPostalCode(context, postalCode); - return true; - } - return false; - } - - /** - * Gets the last stored postal or zip code, which might be decided by {@link LocationUtils} or - * input by users. - */ - public static String getLastPostalCode(Context context) { - return TunerPreferences.getLastPostalCode(context); - } - - /** - * Sets the last stored postal or zip code. This method will overwrite the value written by - * calling {@link #updatePostalCode(Context)}. - */ - public static void setLastPostalCode(Context context, String postalCode) { - Log.i(TAG, "Set Postal Code:" + postalCode); - TunerPreferences.setLastPostalCode(context, postalCode); - } - - @Nullable - private static String getPostalCode(Context context) throws IOException, SecurityException { - Address address = LocationUtils.getCurrentAddress(context); - if (address != null) { - Log.i(TAG, "Current country and postal code is " + address.getCountryName() + ", " - + address.getPostalCode()); - if (TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) { - return address.getPostalCode(); - } - } - return null; - } - - /** An {@link java.lang.Exception} class to notify no valid postal or zip code is available. */ - public static class NoPostalCodeException extends Exception { - public NoPostalCodeException() { - } - } -}
\ No newline at end of file diff --git a/src/com/android/tv/util/StringUtils.java b/src/com/android/tv/tuner/util/StringUtils.java index 659807e2..15571e75 100644 --- a/src/com/android/tv/util/StringUtils.java +++ b/src/com/android/tv/tuner/util/StringUtils.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.util; +package com.android.tv.tuner.util; /** * Utility class for handling {@link String}. diff --git a/src/com/android/tv/tuner/util/SystemPropertiesProxy.java b/src/com/android/tv/tuner/util/SystemPropertiesProxy.java index 2817ccbf..62a64361 100644 --- a/src/com/android/tv/tuner/util/SystemPropertiesProxy.java +++ b/src/com/android/tv/tuner/util/SystemPropertiesProxy.java @@ -58,20 +58,4 @@ public class SystemPropertiesProxy { } return def; } - - public static String getString(String key, String def) throws IllegalArgumentException { - try { - Class SystemPropertiesClass = Class.forName("android.os.SystemProperties"); - Method getIntMethod = - SystemPropertiesClass.getDeclaredMethod("get", String.class, String.class); - getIntMethod.setAccessible(true); - return (String) getIntMethod.invoke(SystemPropertiesClass, key, def); - } catch (InvocationTargetException - | IllegalAccessException - | NoSuchMethodException - | ClassNotFoundException e) { - Log.e(TAG, "Failed to invoke SystemProperties.get()", e); - } - return def; - } } diff --git a/src/com/android/tv/tuner/util/TunerInputInfoUtils.java b/src/com/android/tv/tuner/util/TunerInputInfoUtils.java index fd9ec77d..5c411f64 100644 --- a/src/com/android/tv/tuner/util/TunerInputInfoUtils.java +++ b/src/com/android/tv/tuner/util/TunerInputInfoUtils.java @@ -25,7 +25,6 @@ import android.os.Build; import android.support.annotation.Nullable; import android.support.v4.os.BuildCompat; import android.util.Log; -import android.util.Pair; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.tuner.R; @@ -44,31 +43,23 @@ public class TunerInputInfoUtils { */ @Nullable @TargetApi(Build.VERSION_CODES.N) - public static TvInputInfo buildTunerInputInfo(Context context) { - Pair<Integer, Integer> tunerTypeAndCount = TunerHal.getTunerTypeAndCount(context); - if (tunerTypeAndCount.first == null || tunerTypeAndCount.second == 0) { + public static TvInputInfo buildTunerInputInfo(Context context, boolean fromBuiltInTuner) { + int numOfDevices = TunerHal.getTunerCount(context); + if (numOfDevices == 0) { return null; } - int inputLabelId = 0; - switch (tunerTypeAndCount.first) { - case TunerHal.TUNER_TYPE_BUILT_IN: - inputLabelId = R.string.bt_app_name; - break; - case TunerHal.TUNER_TYPE_USB: - inputLabelId = R.string.ut_app_name; - break; - case TunerHal.TUNER_TYPE_NETWORK: - inputLabelId = R.string.nt_app_name; - break; + TvInputInfo.Builder builder = new TvInputInfo.Builder(context, new ComponentName(context, + TunerTvInputService.class)); + if (fromBuiltInTuner) { + builder.setLabel(R.string.bt_app_name); + } else { + builder.setLabel(R.string.ut_app_name); } try { - TvInputInfo.Builder builder = new TvInputInfo.Builder(context, - new ComponentName(context, TunerTvInputService.class)); - return builder.setLabel(inputLabelId) - .setCanRecord(CommonFeatures.DVR.isEnabled(context)) - .setTunerCount(tunerTypeAndCount.second) + return builder.setCanRecord(CommonFeatures.DVR.isEnabled(context)) + .setTunerCount(numOfDevices) .build(); - } catch (IllegalArgumentException | NullPointerException e) { + } catch (NullPointerException e) { // TunerTvInputService is not enabled. return null; } @@ -82,7 +73,7 @@ public class TunerInputInfoUtils { public static void updateTunerInputInfo(Context context) { if (BuildCompat.isAtLeastN()) { if (DEBUG) Log.d(TAG, "updateTunerInputInfo()"); - TvInputInfo info = buildTunerInputInfo(context); + TvInputInfo info = buildTunerInputInfo(context, isBuiltInTuner(context)); if (info != null) { ((TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE)) .updateTvInputInfo(info); @@ -97,4 +88,13 @@ public class TunerInputInfoUtils { } } } -}
\ No newline at end of file + + /** + * Returns if the current tuner service is for a built-in tuner. + * + * @param context {@link Context} instance + */ + public static boolean isBuiltInTuner(Context context) { + return TunerHal.getTunerType(context) == TunerHal.TUNER_TYPE_BUILT_IN; + } +} diff --git a/src/com/android/tv/ui/AppLayerTvView.java b/src/com/android/tv/ui/AppLayerTvView.java index 6a83c541..09acb36b 100644 --- a/src/com/android/tv/ui/AppLayerTvView.java +++ b/src/com/android/tv/ui/AppLayerTvView.java @@ -22,7 +22,6 @@ import android.util.AttributeSet; import android.view.SurfaceView; import android.view.View; -import com.android.tv.util.Debug; import com.android.tv.experiments.Experiments; /** @@ -60,13 +59,4 @@ public class AppLayerTvView extends TvView { } super.onViewAdded(child); } - - @Override - public void getLocationOnScreen(int[] outLocation) { - super.getLocationOnScreen(outLocation); - - // The TvView.MySessionCallback.onSessionCreated() will call this method indirectly. - Debug.getTimer(Debug.TAG_START_UP_TIMER).log( - "AppLayerTvView.getLocationOnScreen, session created"); - } } diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java index eed536a8..3cf4de83 100644 --- a/src/com/android/tv/ui/ChannelBannerView.java +++ b/src/com/android/tv/ui/ChannelBannerView.java @@ -57,7 +57,7 @@ import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.data.StreamInfo; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.util.ImageCache; import com.android.tv.util.ImageLoader; @@ -98,8 +98,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private static final String EMPTY_STRING = ""; - private Program mNoProgram; - private Program mLockedChannelProgram; + private static Program sNoProgram; + private static Program sLockedChannelProgram; private static String sClosedCaptionMark; private final MainActivity mMainActivity; @@ -123,7 +123,6 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private String mProgramDescriptionText; private View mAnchorView; private Channel mCurrentChannel; - private boolean mCurrentChannelLogoExists; private Program mLastUpdatedProgram; private final Handler mHandler = new Handler(); private final DvrManager mDvrManager; @@ -131,7 +130,6 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private TvContentRating mBlockingContentRating; private int mLockType; - private boolean mUpdateOnTune; private Animator mResizeAnimator; private int mCurrentHeight; @@ -194,7 +192,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage @Override public void run() { removeCallbacks(this); - updateViews(false); + updateViews(null); } }; @@ -245,14 +243,18 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage mContentRatingsManager = TvApplication.getSingletons(getContext()) .getTvInputManagerHelper().getContentRatingsManager(); - mNoProgram = new Program.Builder() - .setTitle(context.getString(R.string.channel_banner_no_title)) - .setDescription(EMPTY_STRING) - .build(); - mLockedChannelProgram = new Program.Builder() - .setTitle(context.getString(R.string.channel_banner_locked_channel_title)) - .setDescription(EMPTY_STRING) - .build(); + if (sNoProgram == null) { + sNoProgram = new Program.Builder() + .setTitle(context.getString(R.string.channel_banner_no_title)) + .setDescription(EMPTY_STRING) + .build(); + } + if (sLockedChannelProgram == null){ + sLockedChannelProgram = new Program.Builder() + .setTitle(context.getString(R.string.channel_banner_locked_channel_title)) + .setDescription(EMPTY_STRING) + .build(); + } if (sClosedCaptionMark == null) { sClosedCaptionMark = context.getString(R.string.closed_caption); } @@ -343,17 +345,19 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage * Set new lock type. * * @param lockType Any of LOCK_NONE, LOCK_PROGRAM_DETAIL, or LOCK_CHANNEL_INFO. - * @return the previous lock type of the channel banner. + * @return {@code true} only if lock type is changed * @throws IllegalArgumentException if lockType is invalid. */ - public int setLockType(int lockType) { + public boolean setLockType(int lockType) { if (lockType != LOCK_NONE && lockType != LOCK_CHANNEL_INFO && lockType != LOCK_PROGRAM_DETAIL) { throw new IllegalArgumentException("No such lock type " + lockType); } - int previousLockType = mLockType; - mLockType = lockType; - return previousLockType; + if (mLockType != lockType) { + mLockType = lockType; + return true; + } + return false; } /** @@ -368,34 +372,31 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage /** * Update channel banner view. * - * @param updateOnTune {@false} denotes the channel banner is updated due to other reasons than - * tuning. The channel info will not be updated in this case. + * @param info A StreamInfo that includes stream information. + * If it's {@code null}, only program information will be updated. */ - public void updateViews(boolean updateOnTune) { + public void updateViews(StreamInfo info) { resetAnimationEffects(); - mUpdateOnTune = updateOnTune; - if (mUpdateOnTune) { + Channel channel = mMainActivity.getCurrentChannel(); + if (!Objects.equals(mCurrentChannel, channel)) { + mBlockingContentRating = null; if (isShown()) { scheduleHide(); } - mBlockingContentRating = null; - mCurrentChannel = mMainActivity.getCurrentChannel(); - mCurrentChannelLogoExists = - mCurrentChannel != null && mCurrentChannel.channelLogoExists(); - updateStreamInfo(null); + } + mCurrentChannel = channel; + 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 != null && channel.equals(info.getCurrentChannel()) ? info + : null); updateChannelInfo(); } updateProgramInfo(mMainActivity.getCurrentProgram()); - mChannelView.setVisibility(VISIBLE); - mUpdateOnTune = false; } - /** - * Update channel banner view with stream info. - * - * @param info A StreamInfo that includes stream information. - */ - public void updateStreamInfo(StreamInfo info) { + private void updateStreamInfo(StreamInfo info) { // Update stream information in a channel. if (mLockType != LOCK_CHANNEL_INFO && info != null) { updateText(mClosedCaptionTextView, info.hasClosedCaption() ? sClosedCaptionMark @@ -413,6 +414,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage mAspectRatioTextView.setVisibility(View.GONE); mResolutionTextView.setVisibility(View.GONE); mAudioChannelTextView.setVisibility(View.GONE); + for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) { + mContentRatingsTextViews[i].setVisibility(View.GONE); + } } } @@ -463,7 +467,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage } mChannelLogoImageView.setImageBitmap(null); mChannelLogoImageView.setVisibility(View.GONE); - if (mCurrentChannel != null && mCurrentChannelLogoExists) { + if (mCurrentChannel != null) { mCurrentChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, mChannelLogoImageViewWidth, mChannelLogoImageViewHeight, createChannelLogoCallback(this, mCurrentChannel)); @@ -546,9 +550,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage if (mResizeAnimator == null) { String description = mProgramDescriptionTextView.getText().toString(); - boolean programDescriptionNeedFadeAnimation = - !description.equals(mProgramDescriptionText) && !mUpdateOnTune; - updateBannerHeight(programDescriptionNeedFadeAnimation); + boolean needFadeAnimation = !description.equals(mProgramDescriptionText); + updateBannerHeight(needFadeAnimation); } else { mProgramInfoUpdatePendingByResizing = true; } @@ -556,9 +559,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private void updateProgramInfo(Program program) { if (mLockType == LOCK_CHANNEL_INFO) { - program = mLockedChannelProgram; + program = sLockedChannelProgram; } else if (program == null || !program.isValid() || TextUtils.isEmpty(program.getTitle())) { - program = mNoProgram; + program = sNoProgram; } if (mLastUpdatedProgram == null @@ -587,9 +590,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage mProgramDescriptionText = program.getDescription(); } String description = mProgramDescriptionTextView.getText().toString(); - boolean programDescriptionNeedFadeAnimation = (isProgramChanged - || !description.equals(mProgramDescriptionText)) && !mUpdateOnTune; - updateBannerHeight(programDescriptionNeedFadeAnimation); + boolean needFadeAnimation = isProgramChanged + || !description.equals(mProgramDescriptionText); + updateBannerHeight(needFadeAnimation); } else { mProgramInfoUpdatePendingByResizing = true; } @@ -600,7 +603,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage if (program == null) { return; } - updateProgramTextView(program == mLockedChannelProgram, program.getTitle(), + updateProgramTextView(program == sLockedChannelProgram, program.getTitle(), program.getEpisodeDisplayTitle(getContext())); } @@ -627,8 +630,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); mProgramTextView.setText(text); } - int width = mProgramDescriptionTextViewWidth + (mCurrentChannelLogoExists ? - 0 : mChannelLogoImageViewWidth + mChannelLogoImageViewMarginStart); + int width = mProgramDescriptionTextViewWidth + - ((mChannelLogoImageView.getVisibility() != View.VISIBLE) + ? 0 : mChannelLogoImageViewWidth + mChannelLogoImageViewMarginStart); ViewGroup.LayoutParams lp = mProgramTextView.getLayoutParams(); lp.width = width; mProgramTextView.setLayoutParams(lp); @@ -651,27 +655,23 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage } private void updateProgramRatings(Program program) { - if (mLockType == LOCK_CHANNEL_INFO) { - for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) { - mContentRatingsTextViews[i].setVisibility(View.GONE); - } - } else if (mBlockingContentRating != null) { + if (mBlockingContentRating != null) { mContentRatingsTextViews[0].setText( mContentRatingsManager.getDisplayNameForRating(mBlockingContentRating)); mContentRatingsTextViews[0].setVisibility(View.VISIBLE); for (int i = 1; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) { mContentRatingsTextViews[i].setVisibility(View.GONE); } - } else { - TvContentRating[] ratings = (program == null) ? null : program.getContentRatings(); - for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) { - if (ratings == null || ratings.length <= i) { - mContentRatingsTextViews[i].setVisibility(View.GONE); - } else { - mContentRatingsTextViews[i].setText( - mContentRatingsManager.getDisplayNameForRating(ratings[i])); - mContentRatingsTextViews[i].setVisibility(View.VISIBLE); - } + return; + } + TvContentRating[] ratings = (program == null) ? null : program.getContentRatings(); + for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) { + if (ratings == null || ratings.length <= i) { + mContentRatingsTextViews[i].setVisibility(View.GONE); + } else { + mContentRatingsTextViews[i].setText( + mContentRatingsManager.getDisplayNameForRating(ratings[i])); + mContentRatingsTextViews[i].setVisibility(View.VISIBLE); } } } @@ -769,7 +769,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage mLastUpdatedProgram = program; } - private void updateBannerHeight(boolean needProgramDescriptionFadeAnimation) { + private void updateBannerHeight(boolean needFadeAnimation) { Assert.assertNull(mResizeAnimator); // Need to measure the layout height with the new description text. CharSequence oldDescription = mProgramDescriptionTextView.getText(); @@ -785,13 +785,12 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage layoutParams.height = targetHeight; setLayoutParams(layoutParams); } - } else if (mCurrentHeight != targetHeight || needProgramDescriptionFadeAnimation) { + } else if (mCurrentHeight != targetHeight || needFadeAnimation) { // Restore description text for fade in/out animation. - if (needProgramDescriptionFadeAnimation) { + if (needFadeAnimation) { mProgramDescriptionTextView.setText(oldDescription); } - mResizeAnimator = - createResizeAnimator(targetHeight, needProgramDescriptionFadeAnimation); + mResizeAnimator = createResizeAnimator(targetHeight, needFadeAnimation); mResizeAnimator.start(); } } diff --git a/src/com/android/tv/ui/KeypadChannelSwitchView.java b/src/com/android/tv/ui/KeypadChannelSwitchView.java index ac5d841d..abc05bad 100644 --- a/src/com/android/tv/ui/KeypadChannelSwitchView.java +++ b/src/com/android/tv/ui/KeypadChannelSwitchView.java @@ -39,7 +39,7 @@ import android.widget.TextView; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.util.DurationTimer; +import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.Tracker; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; diff --git a/src/com/android/tv/ui/SelectInputView.java b/src/com/android/tv/ui/SelectInputView.java index 88872a0a..5e25ae43 100644 --- a/src/com/android/tv/ui/SelectInputView.java +++ b/src/com/android/tv/ui/SelectInputView.java @@ -18,6 +18,7 @@ package com.android.tv.ui; import android.content.Context; import android.content.res.Resources; +import android.hardware.hdmi.HdmiDeviceInfo; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvInputManager.TvInputCallback; @@ -36,13 +37,14 @@ import android.widget.TextView; import com.android.tv.ApplicationSingletons; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.util.DurationTimer; +import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.Tracker; import com.android.tv.data.Channel; import com.android.tv.util.TvInputManagerHelper; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -56,7 +58,7 @@ public class SelectInputView extends VerticalGridView implements private final TvInputManagerHelper mTvInputManagerHelper; private final List<TvInputInfo> mInputList = new ArrayList<>(); - private final TvInputManagerHelper.InputComparator mComparator; + private final InputsComparator mComparator = new InputsComparator(); private final Tracker mTracker; private final DurationTimer mViewDurationTimer = new DurationTimer(); private final TvInputCallback mTvInputCallback = new TvInputCallback() { @@ -147,7 +149,6 @@ public class SelectInputView extends VerticalGridView implements ApplicationSingletons appSingletons = TvApplication.getSingletons(context); mTracker = appSingletons.getTracker(); mTvInputManagerHelper = appSingletons.getTvInputManagerHelper(); - mComparator = new TvInputManagerHelper.InputComparator(context, mTvInputManagerHelper); Resources resources = context.getResources(); mInputItemHeight = resources.getDimensionPixelSize(R.dimen.input_banner_item_height); @@ -384,6 +385,72 @@ public class SelectInputView extends VerticalGridView implements } } + private class InputsComparator implements Comparator<TvInputInfo> { + @Override + public int compare(TvInputInfo lhs, TvInputInfo rhs) { + if (lhs == null) { + return (rhs == null) ? 0 : 1; + } + if (rhs == null) { + return -1; + } + + boolean enabledL = isInputEnabled(lhs); + boolean enabledR = isInputEnabled(rhs); + if (enabledL != enabledR) { + return enabledL ? -1 : 1; + } + + int priorityL = getPriority(lhs); + int priorityR = getPriority(rhs); + if (priorityL != priorityR) { + return priorityR - priorityL; + } + + String customLabelL = (String) lhs.loadCustomLabel(getContext()); + String customLabelR = (String) rhs.loadCustomLabel(getContext()); + if (!TextUtils.equals(customLabelL, customLabelR)) { + customLabelL = customLabelL == null ? "" : customLabelL; + customLabelR = customLabelR == null ? "" : customLabelR; + return customLabelL.compareToIgnoreCase(customLabelR); + } + + String labelL = (String) lhs.loadLabel(getContext()); + String labelR = (String) rhs.loadLabel(getContext()); + labelL = labelL == null ? "" : labelL; + labelR = labelR == null ? "" : labelR; + return labelL.compareToIgnoreCase(labelR); + } + + private int getPriority(TvInputInfo info) { + switch (info.getType()) { + case TvInputInfo.TYPE_TUNER: + return 9; + case TvInputInfo.TYPE_HDMI: + HdmiDeviceInfo hdmiInfo = info.getHdmiDeviceInfo(); + if (hdmiInfo != null && hdmiInfo.isCecDevice()) { + return 8; + } + return 7; + case TvInputInfo.TYPE_DVI: + return 6; + case TvInputInfo.TYPE_COMPONENT: + return 5; + case TvInputInfo.TYPE_SVIDEO: + return 4; + case TvInputInfo.TYPE_COMPOSITE: + return 3; + case TvInputInfo.TYPE_DISPLAY_PORT: + return 2; + case TvInputInfo.TYPE_VGA: + return 1; + case TvInputInfo.TYPE_SCART: + default: + return 0; + } + } + } + /** * A callback interface for the input selection. */ diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java index 8f4f40f5..cbe459fb 100644 --- a/src/com/android/tv/ui/TunableTvView.java +++ b/src/com/android/tv/ui/TunableTvView.java @@ -19,16 +19,9 @@ package com.android.tv.ui; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.TimeInterpolator; -import android.app.AlertDialog; -import android.app.ApplicationErrorReport; +import android.annotation.SuppressLint; import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; import android.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.graphics.PorterDuff; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; import android.media.PlaybackParams; import android.media.tv.TvContentRating; import android.media.tv.TvInputInfo; @@ -45,6 +38,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.text.TextUtils; import android.text.format.DateUtils; import android.util.AttributeSet; @@ -62,20 +56,14 @@ import com.android.tv.InputSessionManager; import com.android.tv.InputSessionManager.TvViewSession; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.data.Program; -import com.android.tv.data.ProgramDataManager; -import com.android.tv.parental.ParentalControlSettings; -import com.android.tv.util.DurationTimer; -import com.android.tv.util.Debug; +import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.Tracker; -import com.android.tv.common.BuildConfig; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; import com.android.tv.data.StreamInfo; import com.android.tv.data.WatchedHistoryManager; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.recommendation.NotificationService; -import com.android.tv.util.ImageLoader; import com.android.tv.util.NetworkUtils; import com.android.tv.util.PermissionUtils; import com.android.tv.util.TvInputManagerHelper; @@ -117,13 +105,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private static final int FADING_IN = 2; private static final int FADING_OUT = 3; + // It is too small to see the description text without PIP_BLOCK_SCREEN_SCALE_FACTOR. + private static final float PIP_BLOCK_SCREEN_SCALE_FACTOR = 1.2f; + private AppLayerTvView mTvView; private TvViewSession mTvViewSession; private Channel mCurrentChannel; private TvInputManagerHelper mInputManagerHelper; private ContentRatingsManager mContentRatingsManager; - private ParentalControlSettings mParentalControlSettings; - private ProgramDataManager mProgramDataManager; @Nullable private WatchedHistoryManager mWatchedHistoryManager; private boolean mStarted; @@ -137,7 +126,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private int mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; private boolean mHasClosedCaption = false; private boolean mVideoAvailable; - private boolean mAudioAvailable; private boolean mScreenBlocked; private OnScreenBlockingChangedListener mOnScreenBlockedListener; private TvContentRating mBlockedContentRating; @@ -148,8 +136,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private boolean mParentControlEnabled; private int mFixedSurfaceWidth; private int mFixedSurfaceHeight; + private boolean mIsPip; + private int mScreenHeight; + private int mShrunkenTvViewHeight; private final boolean mCanModifyParentalControls; - private boolean mIsUnderShrunken; @TimeShiftState private int mTimeShiftState = TIME_SHIFT_STATE_NONE; private TimeShiftListener mTimeShiftListener; @@ -166,15 +156,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo { // A View to hide screen when there's problem in video playback. private final BlockScreenView mHideScreenView; - private final int mHideScreenImageColorFilter; - - // A spinner view to show buffering status. - private final View mBufferingSpinnerView; - private TuningBlockView mTuningBlockView; // A View to block screen until onContentAllowed is received if parental control is on. private final View mBlockScreenForTuneView; + // A spinner view to show buffering status. + private final View mBufferingSpinnerView; + // A View for fade-in/out animation private final View mDimScreenView; private int mFadeState = FADED_IN; @@ -298,66 +286,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo { @Override public void onVideoAvailable(String inputId) { - if (DEBUG) Log.d(TAG, "onVideoAvailable: {inputId=" + inputId + "}"); - Debug.getTimer(Debug.TAG_START_UP_TIMER).log("Start up of Live TV ends," + - " TunableTvView.onVideoAvailable resets timer"); - long startUpDurationTime = Debug.getTimer(Debug.TAG_START_UP_TIMER).reset(); - Debug.removeTimer(Debug.TAG_START_UP_TIMER); - if (BuildConfig.ENG - && startUpDurationTime > Debug.TIME_START_UP_DURATION_THRESHOLD) { - showAlertDialogForLongStartUp(); - } unhideScreenByVideoAvailability(); if (mOnTuneListener != null) { mOnTuneListener.onStreamInfoChanged(TunableTvView.this); } } - private void showAlertDialogForLongStartUp() { - new AlertDialog.Builder(getContext()).setTitle( - getContext().getString(R.string.settings_send_feedback)) - .setMessage("Because the start up time of Live channels is too long," + - " please send feedback") - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - Intent intent = new Intent(Intent.ACTION_APP_ERROR); - ApplicationErrorReport report = new ApplicationErrorReport(); - report.packageName = report.processName = getContext() - .getApplicationContext().getPackageName(); - report.time = System.currentTimeMillis(); - report.type = ApplicationErrorReport.TYPE_CRASH; - - // Add the crash info to add title of feedback automatically. - ApplicationErrorReport.CrashInfo crash = new - ApplicationErrorReport.CrashInfo(); - crash.exceptionClassName = - "Live TV start up takes long time"; - crash.exceptionMessage = - "The start up time of Live TV is too long"; - report.crashInfo = crash; - - intent.putExtra(Intent.EXTRA_BUG_REPORT, report); - getContext().startActivity(intent); - } - }) - .setNegativeButton(android.R.string.no, null) - .show(); - } - @Override public void onVideoUnavailable(String inputId, int reason) { - if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING - && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) { - Debug.getTimer(Debug.TAG_START_UP_TIMER).log( - "TunableTvView.onVideoUnAvailable reason = (" + reason - + ") and removes timer"); - Debug.removeTimer(Debug.TAG_START_UP_TIMER); - } else { - Debug.getTimer(Debug.TAG_START_UP_TIMER).log( - "TunableTvView.onVideoUnAvailable reason = (" + reason + ")"); - } hideScreenByVideoAvailability(inputId, reason); if (mOnTuneListener != null) { mOnTuneListener.onStreamInfoChanged(TunableTvView.this); @@ -375,8 +311,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { @Override public void onContentAllowed(String inputId) { mBlockScreenForTuneView.setVisibility(View.GONE); - mBlockedContentRating = null; - checkBlockScreenAndMuteNeeded(); + unblockScreenByContentRating(); if (mOnTuneListener != null) { mOnTuneListener.onContentAllowed(); } @@ -384,8 +319,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { @Override public void onContentBlocked(String inputId, TvContentRating rating) { - mBlockedContentRating = rating; - checkBlockScreenAndMuteNeeded(); + blockScreenByContentRating(rating); if (mOnTuneListener != null) { mOnTuneListener.onContentBlocked(); } @@ -393,10 +327,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo { @Override public void onTimeShiftStatusChanged(String inputId, int status) { - if (DEBUG) { - Log.d(TAG, "onTimeShiftStatusChanged: {inputId=" + inputId + ", status=" + status + - "}"); - } boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE; setTimeShiftAvailable(available); } @@ -448,8 +378,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mHideScreenView = (BlockScreenView) findViewById(R.id.hide_screen); mHideScreenView.setImageVisibility(false); mBufferingSpinnerView = findViewById(R.id.buffering_spinner); - mHideScreenImageColorFilter = getResources().getColor( - R.color.tvview_block_image_color_filter, null); mBlockScreenForTuneView = findViewById(R.id.block_screen_for_tune); mDimScreenView = findViewById(R.id.dim); mDimScreenView.animate().setListener(new AnimatorListenerAdapter() { @@ -469,23 +397,27 @@ public class TunableTvView extends FrameLayout implements StreamInfo { }); } - public void initialize(AppLayerTvView tvView, TuningBlockView tuningBlockView, - ProgramDataManager programDataManager, TvInputManagerHelper tvInputManagerHelper) { + public void initialize(AppLayerTvView tvView, boolean isPip, int screenHeight, + int shrunkenTvViewHeight) { mTvView = tvView; - mTuningBlockView = tuningBlockView; - mProgramDataManager = programDataManager; - mInputManagerHelper = tvInputManagerHelper; - mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager(); - mParentalControlSettings = tvInputManagerHelper.getParentalControlSettings(); if (mInputSessionManager != null) { mTvViewSession = mInputSessionManager.createTvViewSession(tvView, this, mCallback); } else { mTvView.setCallback(mCallback); } + mIsPip = isPip; + mScreenHeight = screenHeight; + mShrunkenTvViewHeight = shrunkenTvViewHeight; + mTvView.setZOrderOnTop(isPip); copyLayoutParamsToTvView(); } - public void start() { + public void start(TvInputManagerHelper tvInputManagerHelper) { + mInputManagerHelper = tvInputManagerHelper; + mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager(); + if (mStarted) { + return; + } mStarted = true; } @@ -565,13 +497,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mWatchedHistoryManager = watchedHistoryManager; } - /** - * Sets if the TunableTvView is under shrunken. - */ - public void setIsUnderShrunken(boolean isUnderShrunken) { - mIsUnderShrunken = isUnderShrunken; - } - public boolean isPlaying() { return mStarted; } @@ -594,7 +519,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo { * if the state is disconnected or channelId doesn't exist, it returns false. */ public boolean tuneTo(Channel channel, Bundle params, OnTuneListener listener) { - Debug.getTimer(Debug.TAG_START_UP_TIMER).log("TunableTvView.tuneTo"); if (!mStarted) { throw new IllegalStateException("TvView isn't started"); } @@ -636,7 +560,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mVideoDisplayAspectRatio = 0f; mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; mHasClosedCaption = false; - mBlockedContentRating = null; mTimeShiftCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; // To reduce the IPCs, unregister the callback here and register it when necessary. mTvView.setTimeShiftPositionCallback(null); @@ -648,12 +571,12 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } hideScreenByVideoAvailability(mInputInfo.getId(), TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); - updateBlockScreenUI(false); if (mTvViewSession != null) { mTvViewSession.tune(channel, params, listener); } else { mTvView.tune(mInputInfo.getId(), mCurrentChannel.getUri(), params); } + unblockScreenByContentRating(); if (channel.isPassthrough()) { mBlockScreenForTuneView.setVisibility(View.GONE); } else if (mParentControlEnabled) { @@ -792,11 +715,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } @Override - public boolean isVideoOrAudioAvailable() { - return mVideoAvailable || mAudioAvailable; - } - - @Override public int getVideoUnavailableReason() { return mVideoUnavailableReason; } @@ -829,7 +747,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } /** - * Returns if the screen is blocked by {@link #blockOrUnblockScreen(boolean)}. + * Returns if the screen is blocked by {@link #blockScreen()}. */ public boolean isScreenBlocked() { return mScreenBlocked; @@ -848,18 +766,38 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } /** - * Blocks/unblocks current TV screen and mutes. + * Locks current TV screen and mutes. * There would be black screen with lock icon in order to show that * screen block is intended and not an error. * TODO: Accept parameter to show lock icon or not. - * - * @param blockOrUnblock {@code true} to block the screen, or {@code false} to unblock. */ - public void blockOrUnblockScreen(boolean blockOrUnblock) { - mScreenBlocked = blockOrUnblock; + public void blockScreen() { + mScreenBlocked = true; checkBlockScreenAndMuteNeeded(); if (mOnScreenBlockedListener != null) { - mOnScreenBlockedListener.onScreenBlockingChanged(blockOrUnblock); + mOnScreenBlockedListener.onScreenBlockingChanged(true); + } + } + + private void blockScreenByContentRating(TvContentRating rating) { + mBlockedContentRating = rating; + checkBlockScreenAndMuteNeeded(); + } + + @Override + @SuppressLint("RtlHardcoded") + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (mIsPip) { + int height = bottom - top; + float scale; + if (mBlockScreenType == BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW) { + scale = height * PIP_BLOCK_SCREEN_SCALE_FACTOR / mShrunkenTvViewHeight; + } else { + scale = height * PIP_BLOCK_SCREEN_SCALE_FACTOR / mScreenHeight; + } + // TODO: need to get UX confirmation. + mBlockScreenView.scaleContainerView(scale); } } @@ -881,7 +819,17 @@ public class TunableTvView extends FrameLayout implements StreamInfo { || tvViewLp.gravity != lp.gravity || tvViewLp.height != lp.height || tvViewLp.width != lp.width) { - tvViewLp.leftMargin = lp.leftMargin; + if (lp.topMargin == tvViewLp.topMargin && lp.leftMargin == tvViewLp.leftMargin + && !BuildCompat.isAtLeastN()) { + // HACK: If top and left position aren't changed and SurfaceHolder.setFixedSize is + // used, SurfaceView doesn't catch the width and height change. It causes a bug that + // PIP size change isn't shown when PIP is located TOP|LEFT. So we adjust 1 px for + // small size PIP as a workaround. + // Note: This framework issue has been fixed from NYC. + tvViewLp.leftMargin = lp.leftMargin + 1; + } else { + tvViewLp.leftMargin = lp.leftMargin; + } tvViewLp.topMargin = lp.topMargin; tvViewLp.bottomMargin = lp.bottomMargin; tvViewLp.rightMargin = lp.rightMargin; @@ -997,109 +945,96 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private void checkBlockScreenAndMuteNeeded() { updateBlockScreenUI(false); - updateMuteStatus(); + if (mScreenBlocked || mBlockedContentRating != null) { + mute(); + if (mIsPip) { + // If we don't make mTvView invisible, some frames are leaked when a user changes + // PIP layout in options. + // Note: When video is unavailable, we keep the mTvView's visibility, because + // TIS implementation may not send video available with no surface. + mTvView.setVisibility(View.INVISIBLE); + } + } else { + unmuteIfPossible(); + if (mIsPip) { + mTvView.setVisibility(View.VISIBLE); + } + } + } + + public void unblockScreen() { + mScreenBlocked = false; + checkBlockScreenAndMuteNeeded(); + if (mOnScreenBlockedListener != null) { + mOnScreenBlockedListener.onScreenBlockingChanged(false); + } + } + + private void unblockScreenByContentRating() { + mBlockedContentRating = null; + checkBlockScreenAndMuteNeeded(); } @UiThread private void hideScreenByVideoAvailability(String inputId, int reason) { mVideoAvailable = false; - mAudioAvailable = false; mVideoUnavailableReason = reason; if (mInternetCheckTask != null) { mInternetCheckTask.cancel(true); mInternetCheckTask = null; } - if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) { - // Tuning block view will apply animation when unhide screen, so let's end the - // animation if it is running. - mTuningBlockView.endFadeOutAnimator(); - } switch (reason) { case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setImageVisibility(false); mHideScreenView.setText(R.string.tvview_msg_audio_only); - mTuningBlockView.setVisibility(GONE); mBufferingSpinnerView.setVisibility(GONE); - mAudioAvailable = true; + unmuteIfPossible(); break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING: mBufferingSpinnerView.setVisibility(VISIBLE); + mute(); break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setText(R.string.tvview_msg_weak_signal); - mTuningBlockView.setVisibility(GONE); mBufferingSpinnerView.setVisibility(GONE); + mute(); break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING: + mHideScreenView.setVisibility(VISIBLE); + mHideScreenView.setImageVisibility(false); + mHideScreenView.setText(null); mBufferingSpinnerView.setVisibility(VISIBLE); - if (shouldShowImageForTuning()) { - mHideScreenView.setVisibility(GONE); - mTuningBlockView.setVisibility(VISIBLE); - mTuningBlockView.setImageVisibility(false); - showImageForTuning(); - } else { - mHideScreenView.setVisibility(VISIBLE); - mHideScreenView.setImageVisibility(false); - mHideScreenView.setText(null); - mTuningBlockView.setVisibility(GONE); - } + mute(); break; case VIDEO_UNAVAILABLE_REASON_NOT_TUNED: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setImageVisibility(false); mHideScreenView.setText(null); - mTuningBlockView.setVisibility(GONE); mBufferingSpinnerView.setVisibility(GONE); + mute(); break; case VIDEO_UNAVAILABLE_REASON_NO_RESOURCE: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setImageVisibility(false); mHideScreenView.setText(getTuneConflictMessage(inputId)); - mTuningBlockView.setVisibility(GONE); mBufferingSpinnerView.setVisibility(GONE); + mute(); break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: default: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setImageVisibility(false); mHideScreenView.setText(null); - mTuningBlockView.setVisibility(GONE); mBufferingSpinnerView.setVisibility(GONE); + mute(); if (mCurrentChannel != null && !mCurrentChannel.isPhysicalTunerChannel()) { mInternetCheckTask = new InternetCheckTask(); mInternetCheckTask.execute(); } break; } - updateMuteStatus(); - } - - private boolean shouldShowImageForTuning() { - if (getWidth() == 0 || getWidth() == 0 || mCurrentChannel == null || !isBundledInput() - || mIsUnderShrunken || (mParentControlEnabled && (mCurrentChannel.isLocked()))) { - return false; - } - Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId()); - if (currentProgram == null) { - return false; - } - TvContentRating rating = - mParentalControlSettings.getBlockedRating(currentProgram.getContentRatings()); - return !(mParentControlEnabled && rating != null); - } - - private void showImageForTuning() { - mTuningBlockView.setImage(null); - if (mCurrentChannel == null) { - return; - } - Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId()); - if (currentProgram != null) { - currentProgram.loadPosterArt(getContext(), getWidth(), getHeight(), - createProgramPosterArtCallback(mTuningBlockView, mCurrentChannel.getId())); - } } private String getTuneConflictMessage(String inputId) { @@ -1117,43 +1052,25 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private void unhideScreenByVideoAvailability() { mVideoAvailable = true; - mAudioAvailable = true; - mTuningBlockView.hideWithAnimationIfNeeded(); mHideScreenView.setVisibility(GONE); mBufferingSpinnerView.setVisibility(GONE); - updateMuteStatus(); - } - - private void updateMuteStatus() { - // Workaround: TunerTvInputService uses AC3 pass-through implementation, which disables - // audio tracks to enforce the mute request. We don't want to send mute request if we are - // not going to block the screen to prevent the video jankiness resulted by disabling audio - // track before the playback is started. In other way, we should send unmute request before - // the playback is started, because TunerTvInput will remember the muted state and mute - // itself right way when the playback is going to be started, which results the initial - // jankiness, too. - boolean isBundledInput = isBundledInput(); - if ((isBundledInput || mAudioAvailable) && !mScreenBlocked - && mBlockedContentRating == null) { - if (mIsMuted) { - mIsMuted = false; - mTvView.setStreamVolume(mVolume); - } - } else { - if (!mIsMuted) { - if ((mInputInfo == null || isBundledInput) - && !mScreenBlocked && mBlockedContentRating == null) { - return; - } - mIsMuted = true; - mTvView.setStreamVolume(0); - } + unmuteIfPossible(); + } + + private void unmuteIfPossible() { + if (mVideoAvailable && !mScreenBlocked && mBlockedContentRating == null) { + unmute(); } } - private boolean isBundledInput() { - return mInputInfo != null && mInputInfo.getType() == TvInputInfo.TYPE_TUNER - && Utils.isBundledInput(mInputInfo.getId()); + private void mute() { + mIsMuted = true; + mTvView.setStreamVolume(0); + } + + private void unmute() { + mIsMuted = false; + mTvView.setStreamVolume(mVolume); } /** Returns true if this view is faded out. */ @@ -1351,25 +1268,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo { return mTimeShiftCurrentPositionMs; } - private ImageLoader.ImageLoaderCallback<TuningBlockView> createProgramPosterArtCallback( - TuningBlockView view, final long channelId) { - return new ImageLoader.ImageLoaderCallback<TuningBlockView>(view) { - @Override - public void onBitmapLoaded(TuningBlockView view, @Nullable Bitmap posterArt) { - if (posterArt == null || getCurrentChannel() == null - || channelId != getCurrentChannel().getId() - || !shouldShowImageForTuning()) { - return; - } - Drawable drawablePosterArt = new BitmapDrawable(view.getResources(), posterArt); - drawablePosterArt.mutate().setColorFilter( - mHideScreenImageColorFilter, PorterDuff.Mode.SRC_OVER); - view.setImage(drawablePosterArt); - view.setImageVisibility(true); - } - }; - } - /** * Used to receive the time-shift events. */ diff --git a/src/com/android/tv/ui/TuningBlockView.java b/src/com/android/tv/ui/TuningBlockView.java deleted file mode 100644 index 2914b461..00000000 --- a/src/com/android/tv/ui/TuningBlockView.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.ui; - -import android.animation.Animator; -import android.animation.AnimatorInflater; -import android.animation.AnimatorListenerAdapter; -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.widget.FrameLayout; -import android.widget.ImageView; - -import com.android.tv.R; -import com.android.tv.common.SoftPreconditions; - -/** - * A view to block the screen while tuning channels. - */ -public class TuningBlockView extends FrameLayout{ - private final static String TAG = "TuningBlockView"; - - private ImageView mImageView; - private Animator mFadeOut; - - public TuningBlockView(Context context) { - this(context, null, 0); - } - - public TuningBlockView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public TuningBlockView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mImageView = (ImageView) findViewById(R.id.image); - mFadeOut = AnimatorInflater.loadAnimator( - getContext(), R.animator.tuning_block_view_fade_out); - mFadeOut.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - setVisibility(GONE); - } - }); - mFadeOut.setTarget(mImageView); - } - - /** - * Sets image to the image view. This method should be called after finishing inflate the view. - */ - public void setImage(Drawable imageDrawable) { - SoftPreconditions.checkState(mImageView != null, TAG, "imageView is null"); - mImageView.setImageDrawable(imageDrawable); - } - - /** - * Sets the visibility of image view. - * This method should be called after finishing inflate the view. - */ - public void setImageVisibility(boolean visible) { - SoftPreconditions.checkState(mImageView != null, TAG, "imageView is null"); - mImageView.setAlpha(1.0f); - mImageView.setVisibility(visible ? VISIBLE: GONE); - } - - /** - * Returns if the image view is visible. - * This method should be called after finishing inflate the view. - */ - public boolean isImageArtVisible() { - SoftPreconditions.checkState(mImageView != null, TAG, "imageView is null"); - return mImageView.getVisibility() == VISIBLE; - } - - /** - * Hides the view with animation if needed. - */ - public void hideWithAnimationIfNeeded() { - if (getVisibility() == VISIBLE && isImageArtVisible()) { - mFadeOut.start(); - } else { - setVisibility(GONE); - } - } - - /** - * Ends the fade out animator. - */ - public void endFadeOutAnimator() { - if (mFadeOut != null && mFadeOut.isRunning()) { - mFadeOut.end(); - } - } -} diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java index f86e6e95..e14b286b 100644 --- a/src/com/android/tv/ui/TvOverlayManager.java +++ b/src/com/android/tv/ui/TvOverlayManager.java @@ -39,7 +39,6 @@ import com.android.tv.MainActivity.KeyHandlerResultType; import com.android.tv.R; import com.android.tv.TimeShiftManager; import com.android.tv.TvApplication; -import com.android.tv.TvOptionsManager; import com.android.tv.analytics.Tracker; import com.android.tv.common.WeakHandler; import com.android.tv.common.feature.CommonFeatures; @@ -47,14 +46,13 @@ import com.android.tv.common.ui.setup.OnActionClickListener; import com.android.tv.common.ui.setup.SetupFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.data.ChannelDataManager; -import com.android.tv.dialog.DvrHistoryDialogFragment; import com.android.tv.dialog.FullscreenDialogFragment; -import com.android.tv.dialog.HalfSizedDialogFragment; 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.browse.DvrBrowseActivity; +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; @@ -165,7 +163,6 @@ public class TvOverlayManager { private static final Set<String> AVAILABLE_DIALOG_TAGS = new HashSet<>(); static { AVAILABLE_DIALOG_TAGS.add(RecentlyWatchedDialogFragment.DIALOG_TAG); - AVAILABLE_DIALOG_TAGS.add(DvrHistoryDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(PinDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(FullscreenDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(SettingsFragment.LicenseActionItem.DIALOG_TAG); @@ -198,10 +195,10 @@ public class TvOverlayManager { private OnBackStackChangedListener mOnBackStackChangedListener; public TvOverlayManager(MainActivity mainActivity, ChannelTuner channelTuner, - TunableTvView tvView, TvOptionsManager optionsManager, - KeypadChannelSwitchView keypadChannelSwitchView, ChannelBannerView channelBannerView, - InputBannerView inputBannerView, SelectInputView selectInputView, - ViewGroup sceneContainer, ProgramGuideSearchFragment searchFragment) { + TunableTvView tvView, KeypadChannelSwitchView keypadChannelSwitchView, + ChannelBannerView channelBannerView, InputBannerView inputBannerView, + SelectInputView selectInputView, ViewGroup sceneContainer, + ProgramGuideSearchFragment searchFragment) { mMainActivity = mainActivity; mChannelTuner = channelTuner; ApplicationSingletons singletons = TvApplication.getSingletons(mainActivity); @@ -228,8 +225,7 @@ public class TvOverlayManager { }); // Menu MenuView menuView = (MenuView) mainActivity.findViewById(R.id.menu); - mMenu = new Menu(mainActivity, tvView, optionsManager, menuView, - new MenuRowFactory(mainActivity, tvView), + mMenu = new Menu(mainActivity, tvView, menuView, new MenuRowFactory(mainActivity, tvView), new Menu.OnMenuVisibilityChangeListener() { @Override public void onMenuVisibilityChange(boolean visible) { @@ -545,7 +541,7 @@ public class TvOverlayManager { * Shows DVR manager. */ public void showDvrManager() { - Intent intent = new Intent(mMainActivity, DvrBrowseActivity.class); + Intent intent = new Intent(mMainActivity, DvrActivity.class); mMainActivity.startActivity(intent); } @@ -568,14 +564,6 @@ public class TvOverlayManager { } /** - * Shows DVR history dialog. - */ - public void showDvrHistoryDialog() { - showDialogFragment(DvrHistoryDialogFragment.DIALOG_TAG, - new DvrHistoryDialogFragment(), false); - } - - /** * Shows banner view. */ public void showBanner() { @@ -686,7 +674,7 @@ public class TvOverlayManager { } if ((flags & FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS) != 0) { // Keeps side panels. - } else if (mSideFragmentManager.isActive()) { + } else if (mSideFragmentManager.isSidePanelVisible()) { if ((flags & FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY) != 0) { mSideFragmentManager.hideSidePanel(withAnimation); } else { diff --git a/src/com/android/tv/ui/TvViewUiManager.java b/src/com/android/tv/ui/TvViewUiManager.java index 8d3b14f7..bf874fc7 100644 --- a/src/com/android/tv/ui/TvViewUiManager.java +++ b/src/com/android/tv/ui/TvViewUiManager.java @@ -52,8 +52,9 @@ import com.android.tv.data.DisplayMode; import com.android.tv.util.TvSettings; /** - * The TvViewUiManager is responsible for handling UI layouting and animation of main TvView. - * It also control the settings regarding TvView UI such as display mode. + * The TvViewUiManager is responsible for handling UI layouting and animation of main and PIP + * TvViews. It also control the settings regarding TvView UI such as display mode, PIP layout, + * and PIP size. */ public class TvViewUiManager { private static final String TAG = "TvViewManager"; @@ -68,11 +69,18 @@ public class TvViewUiManager { private final Resources mResources; private final FrameLayout mContentView; private final TunableTvView mTvView; + private final TunableTvView mPipView; private final TvOptionsManager mTvOptionsManager; + private final int mTvViewPapWidth; private final int mTvViewShrunkenStartMargin; private final int mTvViewShrunkenEndMargin; + private final int mTvViewPapStartMargin; + private final int mTvViewPapEndMargin; private int mWindowWidth; private int mWindowHeight; + private final int mPipViewHorizontalMargin; + private final int mPipViewTopMargin; + private final int mPipViewBottomMargin; private final SharedPreferences mSharedPreferences; private final TimeInterpolator mLinearOutSlowIn; private final TimeInterpolator mFastOutLinearIn; @@ -105,6 +113,9 @@ public class TvViewUiManager { private boolean mIsUnderShrunkenTvView; private int mTvViewStartMargin; private int mTvViewEndMargin; + private int mPipLayout; + private int mPipSize; + private boolean mPipStarted; private ObjectAnimator mTvViewAnimator; private FrameLayout.LayoutParams mTvViewLayoutParams; // TV view's position when the display mode is FULL. It is used to compute PIP location relative @@ -119,11 +130,12 @@ public class TvViewUiManager { private int mAppliedTvViewEndMargin; private float mAppliedVideoDisplayAspectRatio; - public TvViewUiManager(Context context, TunableTvView tvView, + public TvViewUiManager(Context context, TunableTvView tvView, TunableTvView pipView, FrameLayout contentView, TvOptionsManager tvOptionManager) { mContext = context; mResources = mContext.getResources(); mTvView = tvView; + mPipView = pipView; mContentView = contentView; mTvOptionsManager = tvOptionManager; @@ -135,12 +147,18 @@ public class TvViewUiManager { mWindowWidth = size.x; mWindowHeight = size.y; - // Have an assumption that TvView Shrinking happens only in full screen. + // Have an assumption that PIP and TvView Shrinking happens only in full screen. mTvViewShrunkenStartMargin = mResources .getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_start); mTvViewShrunkenEndMargin = mResources.getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_end) + mResources.getDimensionPixelSize(R.dimen.side_panel_width); + int papMarginHorizontal = mResources + .getDimensionPixelOffset(R.dimen.papview_margin_horizontal); + int papSpacing = mResources.getDimensionPixelOffset(R.dimen.papview_spacing); + mTvViewPapWidth = (mWindowWidth - papSpacing) / 2 - papMarginHorizontal; + mTvViewPapStartMargin = papMarginHorizontal + mTvViewPapWidth + papSpacing; + mTvViewPapEndMargin = papMarginHorizontal; mTvViewFrame = createMarginLayoutParams(0, 0, 0, 0); mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); @@ -149,6 +167,11 @@ public class TvViewUiManager { .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in); mFastOutLinearIn = AnimationUtils .loadInterpolator(mContext, android.R.interpolator.fast_out_linear_in); + + mPipViewHorizontalMargin = mResources + .getDimensionPixelOffset(R.dimen.pipview_margin_horizontal); + mPipViewTopMargin = mResources.getDimensionPixelOffset(R.dimen.pipview_margin_top); + mPipViewBottomMargin = mResources.getDimensionPixelOffset(R.dimen.pipview_margin_bottom); } public void onConfigurationChanged(final int windowWidth, final int windowHeight) { @@ -177,11 +200,18 @@ public class TvViewUiManager { */ public void startShrunkenTvView() { mIsUnderShrunkenTvView = true; - mTvView.setIsUnderShrunken(true); mTvViewStartMarginBeforeShrunken = mTvViewStartMargin; mTvViewEndMarginBeforeShrunken = mTvViewEndMargin; - setTvViewMargin(mTvViewShrunkenStartMargin, mTvViewShrunkenEndMargin); + if (mPipStarted && getPipLayout() == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { + float sidePanelWidth = mResources.getDimensionPixelOffset(R.dimen.side_panel_width); + float factor = 1.0f - sidePanelWidth / mWindowWidth; + int startMargin = (int) (mTvViewPapStartMargin * factor); + int endMargin = (int) (mTvViewPapEndMargin * factor + sidePanelWidth); + setTvViewMargin(startMargin, endMargin); + } else { + setTvViewMargin(mTvViewShrunkenStartMargin, mTvViewShrunkenEndMargin); + } mDisplayModeBeforeShrunken = setDisplayMode(DisplayMode.MODE_NORMAL, false, true); } @@ -191,7 +221,6 @@ public class TvViewUiManager { */ public void endShrunkenTvView() { mIsUnderShrunkenTvView = false; - mTvView.setIsUnderShrunken(false); setTvViewMargin(mTvViewStartMarginBeforeShrunken, mTvViewEndMarginBeforeShrunken); setDisplayMode(mDisplayModeBeforeShrunken, false, true); } @@ -298,6 +327,120 @@ public class TvViewUiManager { } /** + * Returns the current PIP layout. The layout should be one of + * {@link TvSettings#PIP_LAYOUT_BOTTOM_RIGHT}, {@link TvSettings#PIP_LAYOUT_TOP_RIGHT}, + * {@link TvSettings#PIP_LAYOUT_TOP_LEFT}, {@link TvSettings#PIP_LAYOUT_BOTTOM_LEFT} and + * {@link TvSettings#PIP_LAYOUT_SIDE_BY_SIDE}. + */ + public int getPipLayout() { + return mPipLayout; + } + + /** + * Sets the PIP layout. The layout should be one of + * {@link TvSettings#PIP_LAYOUT_BOTTOM_RIGHT}, {@link TvSettings#PIP_LAYOUT_TOP_RIGHT}, + * {@link TvSettings#PIP_LAYOUT_TOP_LEFT}, {@link TvSettings#PIP_LAYOUT_BOTTOM_LEFT} and + * {@link TvSettings#PIP_LAYOUT_SIDE_BY_SIDE}. + * + * @param storeInPreference if true, the stored value will be restored by + * {@link #restorePipLayout()}. + */ + public void setPipLayout(int pipLayout, boolean storeInPreference) { + mPipLayout = pipLayout; + if (storeInPreference) { + TvSettings.setPipLayout(mContext, pipLayout); + } + updatePipView(mTvViewFrame); + if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { + setTvViewMargin(mTvViewPapStartMargin, mTvViewPapEndMargin); + setDisplayMode(DisplayMode.MODE_NORMAL, false, false); + } else { + setTvViewMargin(0, 0); + restoreDisplayMode(false); + } + mTvOptionsManager.onPipLayoutChanged(pipLayout); + } + + /** + * Restores the PIP layout which {@link #setPipLayout} lastly stores. + */ + public void restorePipLayout() { + setPipLayout(TvSettings.getPipLayout(mContext), false); + } + + /** + * Called when PIP is started. + */ + public void onPipStart() { + mPipStarted = true; + updatePipView(); + mPipView.setVisibility(View.VISIBLE); + } + + /** + * Called when PIP is stopped. + */ + public void onPipStop() { + setTvViewMargin(0, 0); + mPipView.setVisibility(View.GONE); + mPipStarted = false; + } + + /** + * Called when PIP is resumed. + */ + public void showPipForResume() { + mPipView.setVisibility(View.VISIBLE); + } + + /** + * Called when PIP is paused. + */ + public void hidePipForPause() { + if (mPipLayout != TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { + mPipView.setVisibility(View.GONE); + } + } + + /** + * Updates PIP view. It is usually called, when video resolution in PIP is updated. + */ + public void updatePipView() { + updatePipView(mTvViewFrame); + } + + /** + * Returns the size of the PIP view. + */ + public int getPipSize() { + return mPipSize; + } + + /** + * Sets PIP size and applies it immediately. + * + * @param pipSize PIP size. The value should be one of {@link TvSettings#PIP_SIZE_BIG} + * and {@link TvSettings#PIP_SIZE_SMALL}. + * @param storeInPreference if true, the stored value will be restored by + * {@link #restorePipSize()}. + */ + public void setPipSize(int pipSize, boolean storeInPreference) { + mPipSize = pipSize; + if (storeInPreference) { + TvSettings.setPipSize(mContext, pipSize); + } + updatePipView(mTvViewFrame); + mTvOptionsManager.onPipSizeChanged(pipSize); + } + + /** + * Restores the PIP size which {@link #setPipSize} lastly stores. + */ + public void restorePipSize() { + setPipSize(TvSettings.getPipSize(mContext), false); + } + + /** * This margins will be applied when applyDisplayMode is called. */ private void setTvViewMargin(int tvViewStartMargin, int tvViewEndMargin) { @@ -397,6 +540,113 @@ public class TvViewUiManager { } else { mTvView.setLayoutParams(layoutParams); } + updatePipView(mTvViewFrame); + } + } + + /** + * The redlines assume that the ratio of the TV screen is 16:9. If the radio is not 16:9, the + * layout of PAP can be broken. + */ + @SuppressLint("RtlHardcoded") + private void updatePipView(MarginLayoutParams tvViewFrame) { + if (!mPipStarted) { + return; + } + int width; + int height; + int startMargin; + int endMargin; + int topMargin; + int bottomMargin; + int gravity; + + if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { + gravity = Gravity.CENTER_VERTICAL | Gravity.START; + height = tvViewFrame.height; + float videoDisplayAspectRatio = mPipView.getVideoDisplayAspectRatio(); + if (videoDisplayAspectRatio <= 0f) { + width = tvViewFrame.width; + } else { + width = (int) (height * videoDisplayAspectRatio); + if (width > tvViewFrame.width) { + width = tvViewFrame.width; + } + } + startMargin = mResources.getDimensionPixelOffset(R.dimen.papview_margin_horizontal) + * tvViewFrame.width / mTvViewPapWidth + (tvViewFrame.width - width) / 2; + endMargin = 0; + topMargin = 0; + bottomMargin = 0; + } else { + int tvViewWidth = tvViewFrame.width; + int tvViewHeight = tvViewFrame.height; + int tvStartMargin = tvViewFrame.getMarginStart(); + int tvEndMargin = tvViewFrame.getMarginEnd(); + int tvTopMargin = tvViewFrame.topMargin; + int tvBottomMargin = tvViewFrame.bottomMargin; + float horizontalScaleFactor = (float) tvViewWidth / mWindowWidth; + float verticalScaleFactor = (float) tvViewHeight / mWindowHeight; + + int maxWidth; + if (mPipSize == TvSettings.PIP_SIZE_SMALL) { + maxWidth = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_small_size_width) + * horizontalScaleFactor); + height = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_small_size_height) + * verticalScaleFactor); + } else if (mPipSize == TvSettings.PIP_SIZE_BIG) { + maxWidth = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_large_size_width) + * horizontalScaleFactor); + height = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_large_size_height) + * verticalScaleFactor); + } else { + throw new IllegalArgumentException("Invalid PIP size: " + mPipSize); + } + float videoDisplayAspectRatio = mPipView.getVideoDisplayAspectRatio(); + if (videoDisplayAspectRatio <= 0f) { + width = maxWidth; + } else { + width = (int) (height * videoDisplayAspectRatio); + if (width > maxWidth) { + width = maxWidth; + } + } + + startMargin = tvStartMargin + (int) (mPipViewHorizontalMargin * horizontalScaleFactor); + endMargin = tvEndMargin + (int) (mPipViewHorizontalMargin * horizontalScaleFactor); + topMargin = tvTopMargin + (int) (mPipViewTopMargin * verticalScaleFactor); + bottomMargin = tvBottomMargin + (int) (mPipViewBottomMargin * verticalScaleFactor); + + switch (mPipLayout) { + case TvSettings.PIP_LAYOUT_TOP_LEFT: + gravity = Gravity.TOP | Gravity.LEFT; + break; + case TvSettings.PIP_LAYOUT_TOP_RIGHT: + gravity = Gravity.TOP | Gravity.RIGHT; + break; + case TvSettings.PIP_LAYOUT_BOTTOM_LEFT: + gravity = Gravity.BOTTOM | Gravity.LEFT; + break; + case TvSettings.PIP_LAYOUT_BOTTOM_RIGHT: + gravity = Gravity.BOTTOM | Gravity.RIGHT; + break; + default: + throw new IllegalArgumentException("Invalid PIP location: " + mPipLayout); + } + } + + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPipView.getLayoutParams(); + if (lp.width != width || lp.height != height || lp.getMarginStart() != startMargin + || lp.getMarginEnd() != endMargin || lp.topMargin != topMargin + || lp.bottomMargin != bottomMargin || lp.gravity != gravity) { + lp.width = width; + lp.height = height; + lp.setMarginStart(startMargin); + lp.setMarginEnd(endMargin); + lp.topMargin = topMargin; + lp.bottomMargin = bottomMargin; + lp.gravity = gravity; + mPipView.setLayoutParams(lp); } } @@ -446,6 +696,7 @@ public class TvViewUiManager { mLastAnimatedTvViewFrame = new MarginLayoutParams(0, 0); interpolateMarginsRelative(mLastAnimatedTvViewFrame, mOldTvViewFrame, mTvViewFrame, fraction); + updatePipView(mLastAnimatedTvViewFrame); } }); } diff --git a/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java b/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java index e5c23372..d6ccdf6b 100644 --- a/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java +++ b/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java @@ -82,9 +82,8 @@ public class ClosedCaptionFragment extends SideFragment { } mItems.add(item); - for (int i = 0; i < tracks.size(); i++) { - final TvTrackInfo track = tracks.get(i); - item = new ClosedCaptionOptionItem(getLabel(track, i), + for (final TvTrackInfo track : tracks) { + item = new ClosedCaptionOptionItem(getLabel(track), CaptionSettings.OPTION_ON, track.getId(), track.getLanguage()); if (isEnabled && track.getId().equals(trackId)) { item.setChecked(true); @@ -173,11 +172,11 @@ public class ClosedCaptionFragment extends SideFragment { super.onDestroyView(); } - private String getLabel(TvTrackInfo track, int trackIndex) { + private String getLabel(TvTrackInfo track) { if (track.getLanguage() != null) { return new Locale(track.getLanguage()).getDisplayName(); } - return getString(R.string.closed_caption_unknown_language, trackIndex + 1); + return getString(R.string.default_language); } private class ClosedCaptionOptionItem extends RadioButtonItem { diff --git a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java index fac24696..0d189cca 100644 --- a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java +++ b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java @@ -18,6 +18,8 @@ package com.android.tv.ui.sidepanel; import android.accounts.Account; import android.app.Activity; +import android.app.ApplicationErrorReport; +import android.content.Intent; import android.support.annotation.NonNull; import android.util.Log; import android.widget.Toast; @@ -25,7 +27,6 @@ import android.widget.Toast; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.BuildConfig; -import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.epg.EpgFetcher; import com.android.tv.experiments.Experiments; import com.android.tv.tuner.TunerPreferences; @@ -53,14 +54,6 @@ public class DeveloperOptionFragment extends SideFragment { @Override protected List<Item> getItemList() { List<Item> items = new ArrayList<>(); - if (CommonFeatures.DVR.isEnabled(getContext())) { - items.add(new ActionItem(getString(R.string.dev_item_dvr_history)) { - @Override - protected void onSelected() { - getMainActivity().getOverlayManager().showDvrHistoryDialog(); - } - }); - } if (BuildConfig.ENG) { items.add(new ActionItem(getString(R.string.dev_item_watch_history)) { @Override @@ -69,6 +62,18 @@ public class DeveloperOptionFragment extends SideFragment { } }); } + items.add(new ActionItem(getString(R.string.dev_item_send_feedback)) { + @Override + protected void onSelected() { + Intent intent = new Intent(Intent.ACTION_APP_ERROR); + ApplicationErrorReport report = new ApplicationErrorReport(); + report.packageName = report.processName = getContext().getPackageName(); + report.time = System.currentTimeMillis(); + report.type = ApplicationErrorReport.TYPE_NONE; + intent.putExtra(Intent.EXTRA_BUG_REPORT, report); + startActivityForResult(intent, 0); + } + }); items.add(new SwitchItem(getString(R.string.dev_item_store_ts_on), getString(R.string.dev_item_store_ts_off), getString(R.string.dev_item_store_ts_description)) { diff --git a/src/com/android/tv/ui/sidepanel/Item.java b/src/com/android/tv/ui/sidepanel/Item.java index 4e47e75b..00f16427 100644 --- a/src/com/android/tv/ui/sidepanel/Item.java +++ b/src/com/android/tv/ui/sidepanel/Item.java @@ -24,7 +24,6 @@ import android.view.ViewGroup; public abstract class Item { private View mItemView; private boolean mEnabled = true; - private boolean mClickable = true; public void setEnabled(boolean enabled) { if (mEnabled != enabled) { @@ -36,16 +35,6 @@ public abstract class Item { } /** - * Sets the item to be clickable or not. - */ - public void setClickable(boolean clickable) { - mClickable = clickable; - if (mItemView != null) { - mItemView.setClickable(clickable); - } - } - - /** * Returns whether this item is enabled. */ public boolean isEnabled() { @@ -75,7 +64,6 @@ public abstract class Item { */ protected void onUpdate() { setEnabledInternal(mItemView, mEnabled); - mItemView.setClickable(mClickable); } protected abstract void onSelected(); diff --git a/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java b/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java new file mode 100644 index 00000000..dec017a8 --- /dev/null +++ b/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java @@ -0,0 +1,170 @@ +/* + * 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.media.tv.TvInputInfo; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.R; +import com.android.tv.util.PipInputManager; +import com.android.tv.util.PipInputManager.PipInput; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class PipInputSelectorFragment extends SideFragment { + private static final String TAG = "PipInputSelector"; + private static final String TRACKER_LABEL = "PIP input source"; + + private final List<Item> mInputItems = new ArrayList<>(); + private PipInputManager mPipInputManager; + private PipInput mInitialPipInput; + private boolean mSelected; + + private final PipInputManager.Listener mPipInputListener = new PipInputManager.Listener() { + @Override + public void onPipInputStateUpdated() { + notifyDataSetChanged(); + } + + @Override + public void onPipInputListUpdated() { + refreshInputList(); + setItems(mInputItems); + } + }; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + mPipInputManager = getMainActivity().getPipInputManager(); + mPipInputManager.addListener(mPipInputListener); + getMainActivity().startShrunkenTvView(false, false); + return super.onCreateView(inflater, container, savedInstanceState); + } + + @Override + public void onStart() { + super.onStart(); + mInitialPipInput = mPipInputManager.getPipInput(getMainActivity().getPipChannel()); + if (mInitialPipInput == null) { + Log.w(TAG, "PIP should be on"); + closeFragment(); + } + int count = 0; + for (Item item : mInputItems) { + InputItem inputItem = (InputItem) item; + if (Objects.equals(inputItem.mPipInput, mInitialPipInput)) { + setSelectedPosition(count); + break; + } + ++count; + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mPipInputManager.removeListener(mPipInputListener); + if (!mSelected) { + getMainActivity().tuneToChannelForPip(mInitialPipInput.getChannel()); + } + getMainActivity().endShrunkenTvView(); + } + + @Override + protected String getTitle() { + return getString(R.string.side_panel_title_pip_input_source); + } + + @Override + public String getTrackerLabel() { + return TRACKER_LABEL; + } + + @Override + protected List<Item> getItemList() { + refreshInputList(); + return mInputItems; + } + + private void refreshInputList() { + mInputItems.clear(); + for (PipInput input : mPipInputManager.getPipInputList(false)) { + mInputItems.add(new InputItem(input)); + } + } + + private class InputItem extends RadioButtonItem { + private final PipInput mPipInput; + + private InputItem(PipInput input) { + super(input.getLongLabel()); + mPipInput = input; + setEnabled(isAvailable()); + } + + @Override + protected void onUpdate() { + super.onUpdate(); + setEnabled(mPipInput.isAvailable()); + setChecked(mPipInput == mInitialPipInput); + } + + @Override + protected void onFocused() { + super.onFocused(); + if (isEnabled()) { + getMainActivity().tuneToChannelForPip(mPipInput.getChannel()); + } + } + + @Override + protected void onSelected() { + super.onSelected(); + if (isEnabled()) { + mSelected = true; + closeFragment(); + } + } + + private boolean isAvailable() { + if (!mPipInput.isAvailable()) { + return false; + } + + // 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.) + 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()) + && !TextUtils.equals(mainInputInfo.getParentId(), pipInputInfo.getParentId()); + } + } +} diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java index f6aa4f86..e8033a22 100644 --- a/src/com/android/tv/ui/sidepanel/SettingsFragment.java +++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java @@ -16,8 +16,6 @@ package com.android.tv.ui.sidepanel; -import android.app.ApplicationErrorReport; -import android.content.Intent; import android.view.View; import android.widget.Toast; @@ -151,26 +149,12 @@ public class SettingsFragment extends SideFragment { // But, we may be able to turn on channel lock feature regardless of the permission. // It's TBD. } - items.add(new ActionItem(getString(R.string.settings_send_feedback)) { - @Override - protected void onSelected() { - Intent intent = new Intent(Intent.ACTION_APP_ERROR); - ApplicationErrorReport report = new ApplicationErrorReport(); - report.packageName = report.processName = getContext().getPackageName(); - report.time = System.currentTimeMillis(); - report.type = ApplicationErrorReport.TYPE_NONE; - intent.putExtra(Intent.EXTRA_BUG_REPORT, report); - startActivityForResult(intent, 0); - } - }); if (LicenseUtils.hasLicenses(activity.getAssets())) { items.add(new LicenseActionItem(activity)); } // Show version. - SimpleActionItem version = new SimpleActionItem(getString(R.string.settings_menu_version), - ((TvApplication) activity.getApplicationContext()).getVersionName()); - version.setClickable(false); - items.add(version); + items.add(new SimpleItem(getString(R.string.settings_menu_version), + ((TvApplication) activity.getApplicationContext()).getVersionName())); return items; } diff --git a/src/com/android/tv/ui/sidepanel/SideFragment.java b/src/com/android/tv/ui/sidepanel/SideFragment.java index bb815eb8..8df56cd2 100644 --- a/src/com/android/tv/ui/sidepanel/SideFragment.java +++ b/src/com/android/tv/ui/sidepanel/SideFragment.java @@ -26,26 +26,24 @@ import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.LinearLayout; import android.widget.TextView; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.util.DurationTimer; +import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.HasTrackerLabel; import com.android.tv.analytics.Tracker; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.ProgramDataManager; import com.android.tv.util.SystemProperties; -import com.android.tv.util.ViewCache; import java.util.List; public abstract class SideFragment extends Fragment implements HasTrackerLabel { public static final int INVALID_POSITION = -1; - private static final int PRELOADED_VIEW_SIZE = 7; + private static final int RECYCLED_VIEW_POOL_SIZE = 7; private static final int[] PRELOADED_VIEW_IDS = { R.layout.option_item_radio_button, R.layout.option_item_channel_lock, @@ -53,8 +51,7 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel { R.layout.option_item_channel_check }; - private static RecyclerView.RecycledViewPool sRecycledViewPool = - new RecyclerView.RecycledViewPool(); + private static RecyclerView.RecycledViewPool sRecycledViewPool; private VerticalGridView mListView; private ItemAdapter mAdapter; @@ -92,6 +89,13 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + if (sRecycledViewPool == null) { + // sRecycledViewPool should be initialized by calling preloadRecycledViews() + // before the entering animation of this fragment starts, + // because it takes long time and if it is called after the animation starts (e.g. here) + // it can affect the animation. + throw new IllegalStateException("The RecyclerView pool has not been initialized."); + } View view = inflater.inflate(getFragmentLayoutResourceId(), container, false); TextView textView = (TextView) view.findViewById(R.id.side_panel_title); @@ -232,27 +236,30 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel { } /** - * Preloads the item views. + * Preloads the view holders. */ - public static void preloadItemViews(Context context) { + public static void preloadRecycledViews(Context context) { + if (sRecycledViewPool != null) { + return; + } + sRecycledViewPool = new RecyclerView.RecycledViewPool(); LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - // Use a fake parent to make the layoutParams set correctly. - ViewGroup fakeParent = new LinearLayout(context); for (int id : PRELOADED_VIEW_IDS) { - sRecycledViewPool.setMaxRecycledViews(id, PRELOADED_VIEW_SIZE); - for (int j = 0; j < PRELOADED_VIEW_SIZE; ++j) { - View view = inflater.inflate(id, fakeParent, false); - ViewCache.getInstance().putView(id, view); + sRecycledViewPool.setMaxRecycledViews(id, RECYCLED_VIEW_POOL_SIZE); + for (int j = 0; j < RECYCLED_VIEW_POOL_SIZE; ++j) { + ItemAdapter.ViewHolder viewHolder = new ItemAdapter.ViewHolder( + inflater.inflate(id, null, false)); + sRecycledViewPool.putRecycledView(viewHolder); } } } /** - * Releases the recycled view pool. + * Releases the pre-loaded view holders. */ - public static void releaseRecycledViewPool() { - sRecycledViewPool.clear(); + public static void releasePreloadedRecycledViews() { + sRecycledViewPool = null; } private static class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ViewHolder> { @@ -271,11 +278,7 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel { @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = ViewCache.getInstance().getView(viewType); - if (view == null) { - view = mLayoutInflater.inflate(viewType, parent, false); - } - return new ViewHolder(view); + return new ViewHolder(mLayoutInflater.inflate(viewType, parent, false)); } @Override diff --git a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java index 4398b3f3..553cd9d7 100644 --- a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java +++ b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java @@ -99,6 +99,7 @@ public class SideFragmentManager { * Shows the given {@link SideFragment}. */ public void show(SideFragment sideFragment, boolean showEnterAnimation) { + SideFragment.preloadRecycledViews(mActivity); if (isHiding()) { mHideAnimator.end(); } @@ -177,6 +178,7 @@ public class SideFragmentManager { * @param withAnimation specifies if animation should be shown. */ public void showSidePanel(boolean withAnimation) { + SideFragment.preloadRecycledViews(mActivity); if (mFragmentCount == 0) { return; } diff --git a/src/com/android/tv/ui/sidepanel/SimpleActionItem.java b/src/com/android/tv/ui/sidepanel/SimpleItem.java index 42553b66..52a5f13f 100644 --- a/src/com/android/tv/ui/sidepanel/SimpleActionItem.java +++ b/src/com/android/tv/ui/sidepanel/SimpleItem.java @@ -19,12 +19,12 @@ package com.android.tv.ui.sidepanel; /** * A simple item which shows title and description. */ -public class SimpleActionItem extends ActionItem { - public SimpleActionItem(String title) { +public class SimpleItem extends ActionItem { + public SimpleItem(String title) { super(title); } - public SimpleActionItem(String title, String description) { + public SimpleItem(String title, String description) { super(title, description); } diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java index 3cc91e40..78243642 100644 --- a/src/com/android/tv/util/AsyncDbTask.java +++ b/src/com/android/tv/util/AsyncDbTask.java @@ -31,7 +31,7 @@ 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.data.RecordedProgram; +import com.android.tv.dvr.RecordedProgram; import java.util.ArrayList; import java.util.List; diff --git a/src/com/android/tv/util/Debug.java b/src/com/android/tv/util/Debug.java deleted file mode 100644 index 67a2683d..00000000 --- a/src/com/android/tv/util/Debug.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.util; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -/** - * A class only for help developers. - */ -public class Debug { - /** - * A threshold of start up time, when the start up time of Live TV is more than it, - * a warning will show to the developer. - */ - public static final long TIME_START_UP_DURATION_THRESHOLD = TimeUnit.SECONDS.toMillis(6); - /** - * Tag for measuring start up time of Live TV. - */ - public static final String TAG_START_UP_TIMER = "start_up_timer"; - - /** - * A global map for duration timers. - */ - private final static Map<String, DurationTimer> sTimerMap = new HashMap<>(); - - /** - * Returns the global duration timer by tag. - */ - public static DurationTimer getTimer(String tag) { - if (sTimerMap.get(tag) != null) { - return sTimerMap.get(tag); - } - DurationTimer timer = new DurationTimer(tag, true); - sTimerMap.put(tag, timer); - return timer; - } - - /** - * Removes the global duration timer by tag. - */ - public static DurationTimer removeTimer(String tag) { - return sTimerMap.remove(tag); - } -} diff --git a/src/com/android/tv/util/LocationUtils.java b/src/com/android/tv/util/LocationUtils.java index 2ae6db18..8e3b59e9 100644 --- a/src/com/android/tv/util/LocationUtils.java +++ b/src/com/android/tv/util/LocationUtils.java @@ -16,9 +16,7 @@ package com.android.tv.util; -import android.Manifest; import android.content.Context; -import android.content.pm.PackageManager; import android.location.Address; import android.location.Geocoder; import android.location.Location; @@ -27,7 +25,6 @@ import android.location.LocationManager; import android.os.Bundle; import android.util.Log; -import com.android.tv.tuner.util.PostalCodeUtils; import java.io.IOException; import java.util.List; @@ -42,7 +39,6 @@ public class LocationUtils { private static Context sApplicationContext; private static Address sAddress; - private static String sCountry; private static IOException sError; /** @@ -63,19 +59,6 @@ public class LocationUtils { return null; } - /** - * Returns the current country. - */ - public static synchronized String getCurrentCountry(Context context) { - if (sCountry != null) { - return sCountry; - } - if (sCountry == null) { - sCountry = context.getResources().getConfiguration().locale.getCountry(); - } - return sCountry; - } - private static void updateAddress(Location location) { if (DEBUG) Log.d(TAG, "Updating address with " + location); if (location == null) { @@ -85,14 +68,9 @@ public class LocationUtils { try { List<Address> addresses = geocoder.getFromLocation( location.getLatitude(), location.getLongitude(), 1); - if (addresses != null && !addresses.isEmpty()) { + if (addresses != null) { sAddress = addresses.get(0); if (DEBUG) Log.d(TAG, "Got " + sAddress); - try { - PostalCodeUtils.updatePostalCode(sApplicationContext); - } catch (Exception e) { - // Do nothing - } } else { if (DEBUG) Log.d(TAG, "No address returned"); } diff --git a/src/com/android/tv/util/Partner.java b/src/com/android/tv/util/Partner.java deleted file mode 100644 index e3688392..00000000 --- a/src/com/android/tv/util/Partner.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (C) 2017 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.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.media.tv.TvInputInfo; -import android.text.TextUtils; -import android.util.Log; - -import java.util.HashMap; -import java.util.Map; - -/** - * This file refers to Partner.java in LeanbackLauncher. Interact with partner customizations. There - * can only be one set of customizations on a device, and it must be bundled with the system. - */ -public class Partner { - private static final String TAG = "Partner"; - /** Marker action used to discover partner */ - private static final String ACTION_PARTNER_CUSTOMIZATION = - "com.google.android.leanbacklauncher.action.PARTNER_CUSTOMIZATION"; - - /** ID tags for device input types */ - public static final String INPUT_TYPE_BUNDLED_TUNER = "input_type_combined_tuners"; - public static final String INPUT_TYPE_TUNER = "input_type_tuner"; - public static final String INPUT_TYPE_CEC_LOGICAL = "input_type_cec_logical"; - public static final String INPUT_TYPE_CEC_RECORDER = "input_type_cec_recorder"; - public static final String INPUT_TYPE_CEC_PLAYBACK = "input_type_cec_playback"; - public static final String INPUT_TYPE_MHL_MOBILE = "input_type_mhl_mobile"; - public static final String INPUT_TYPE_HDMI = "input_type_hdmi"; - public static final String INPUT_TYPE_DVI = "input_type_dvi"; - public static final String INPUT_TYPE_COMPONENT = "input_type_component"; - public static final String INPUT_TYPE_SVIDEO = "input_type_svideo"; - public static final String INPUT_TYPE_COMPOSITE = "input_type_composite"; - public static final String INPUT_TYPE_DISPLAY_PORT = "input_type_displayport"; - public static final String INPUT_TYPE_VGA = "input_type_vga"; - public static final String INPUT_TYPE_SCART = "input_type_scart"; - public static final String INPUT_TYPE_OTHER = "input_type_other"; - - private static final String INPUTS_ORDER = "home_screen_inputs_ordering"; - private static final String TYPE_ARRAY = "array"; - - private static Partner sPartner; - private static final Object sLock = new Object(); - - private final String mPackageName; - private final String mReceiverName; - private final Resources mResources; - - private static final Map<String, Integer> INPUT_TYPE_MAP = new HashMap<>(); - static { - INPUT_TYPE_MAP.put(INPUT_TYPE_BUNDLED_TUNER, TvInputManagerHelper.TYPE_BUNDLED_TUNER); - INPUT_TYPE_MAP.put(INPUT_TYPE_TUNER, TvInputInfo.TYPE_TUNER); - INPUT_TYPE_MAP.put(INPUT_TYPE_CEC_LOGICAL, TvInputManagerHelper.TYPE_CEC_DEVICE); - INPUT_TYPE_MAP.put(INPUT_TYPE_CEC_RECORDER, TvInputManagerHelper.TYPE_CEC_DEVICE_RECORDER); - INPUT_TYPE_MAP.put(INPUT_TYPE_CEC_PLAYBACK, TvInputManagerHelper.TYPE_CEC_DEVICE_PLAYBACK); - INPUT_TYPE_MAP.put(INPUT_TYPE_MHL_MOBILE, TvInputManagerHelper.TYPE_MHL_MOBILE); - INPUT_TYPE_MAP.put(INPUT_TYPE_HDMI, TvInputInfo.TYPE_HDMI); - INPUT_TYPE_MAP.put(INPUT_TYPE_DVI, TvInputInfo.TYPE_DVI); - INPUT_TYPE_MAP.put(INPUT_TYPE_COMPONENT, TvInputInfo.TYPE_COMPONENT); - INPUT_TYPE_MAP.put(INPUT_TYPE_SVIDEO, TvInputInfo.TYPE_SVIDEO); - INPUT_TYPE_MAP.put(INPUT_TYPE_COMPOSITE, TvInputInfo.TYPE_COMPOSITE); - INPUT_TYPE_MAP.put(INPUT_TYPE_DISPLAY_PORT, TvInputInfo.TYPE_DISPLAY_PORT); - INPUT_TYPE_MAP.put(INPUT_TYPE_VGA, TvInputInfo.TYPE_VGA); - INPUT_TYPE_MAP.put(INPUT_TYPE_SCART, TvInputInfo.TYPE_SCART); - INPUT_TYPE_MAP.put(INPUT_TYPE_OTHER, TvInputInfo.TYPE_OTHER); - } - - private Partner(String packageName, String receiverName, Resources res) { - mPackageName = packageName; - mReceiverName = receiverName; - mResources = res; - } - - /** Returns partner instance. */ - public static Partner getInstance(Context context) { - PackageManager pm = context.getPackageManager(); - synchronized (sLock) { - ResolveInfo info = getPartnerResolveInfo(pm); - if (info != null) { - final String packageName = info.activityInfo.packageName; - final String receiverName = info.activityInfo.name; - try { - final Resources res = pm.getResourcesForApplication(packageName); - sPartner = new Partner(packageName, receiverName, res); - sPartner.sendInitBroadcast(context); - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Failed to find resources for " + packageName); - } - } - if (sPartner == null) { - sPartner = new Partner(null, null, null); - } - } - return sPartner; - } - - /** Resets the Partner instance to handle the partner package has changed. */ - public static void reset(Context context, String packageName) { - synchronized (sLock) { - if (sPartner != null && !TextUtils.isEmpty(packageName)) { - if (packageName.equals(sPartner.mPackageName)) { - // Force a refresh, so we send an Init to the updated package - sPartner = null; - getInstance(context); - } - } - } - } - - /** This method is used to send init broadcast to the new/changed partner package. */ - private void sendInitBroadcast(Context context) { - if (!TextUtils.isEmpty(mPackageName) && !TextUtils.isEmpty(mReceiverName)) { - Intent intent = new Intent(ACTION_PARTNER_CUSTOMIZATION); - final ComponentName componentName = new ComponentName(mPackageName, mReceiverName); - intent.setComponent(componentName); - intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); - context.sendBroadcast(intent); - } - } - - /** Returns the order of inputs. */ - public Map<Integer, Integer> getInputsOrderMap() { - HashMap<Integer, Integer> map = new HashMap<>(); - if (mResources != null && !TextUtils.isEmpty(mPackageName)) { - String[] inputsArray = null; - final int resId = mResources.getIdentifier(INPUTS_ORDER, TYPE_ARRAY, mPackageName); - if (resId != 0) { - inputsArray = mResources.getStringArray(resId); - } - if (inputsArray != null) { - int priority = 0; - for (String input : inputsArray) { - Integer type = INPUT_TYPE_MAP.get(input); - if (type != null) { - map.put(type, priority++); - } - } - } - } - return map; - } - - private static ResolveInfo getPartnerResolveInfo(PackageManager pm) { - final Intent intent = new Intent(ACTION_PARTNER_CUSTOMIZATION); - ResolveInfo partnerInfo = null; - for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) { - if (isSystemApp(info)) { - partnerInfo = info; - break; - } - } - return partnerInfo; - } - - protected static boolean isSystemApp(ResolveInfo info) { - return (info.activityInfo != null - && info.activityInfo.applicationInfo != null - && (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0); - } -} diff --git a/src/com/android/tv/util/PipInputManager.java b/src/com/android/tv/util/PipInputManager.java new file mode 100644 index 00000000..2c51d5a0 --- /dev/null +++ b/src/com/android/tv/util/PipInputManager.java @@ -0,0 +1,432 @@ +/* + * 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.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.data.Channel; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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"; + + // Tuner inputs aren't distinguished each other in PipInput. They are handled as one input. + // Therefore, we define a fake input id for the unified input. + private static final String TUNER_INPUT_ID = "tuner_input_id"; + + private final Context mContext; + private final TvInputManagerHelper mInputManager; + private final ChannelTuner mChannelTuner; + private boolean mStarted; + private final Map<String, PipInput> mPipInputMap = new HashMap<>(); // inputId -> PipInput + private final Set<Listener> mListeners = new ArraySet<>(); + + private final TvInputCallback mTvInputCallback = new TvInputCallback() { + @Override + public void onInputAdded(String inputId) { + TvInputInfo input = mInputManager.getTvInputInfo(inputId); + if (input.isPassthroughInput()) { + boolean available = mInputManager.getInputState(input) + == TvInputManager.INPUT_STATE_CONNECTED; + mPipInputMap.put(inputId, new PipInput(inputId, available)); + } else if (!mPipInputMap.containsKey(TUNER_INPUT_ID)) { + boolean available = mChannelTuner.getBrowsableChannelCount() != 0; + mPipInputMap.put(TUNER_INPUT_ID, new PipInput(TUNER_INPUT_ID, available)); + } else { + return; + } + for (Listener l : mListeners) { + l.onPipInputListUpdated(); + } + } + + @Override + public void onInputRemoved(String inputId) { + PipInput pipInput = mPipInputMap.remove(inputId); + if (pipInput == null) { + if (!mPipInputMap.containsKey(TUNER_INPUT_ID)) { + Log.w(TAG, "A TV input (" + inputId + ") isn't tracked in PipInputManager"); + return; + } + if (mInputManager.getTunerTvInputSize() > 0) { + return; + } + mPipInputMap.remove(TUNER_INPUT_ID); + } + for (Listener l : mListeners) { + l.onPipInputListUpdated(); + } + } + + @Override + public void onInputStateChanged(String inputId, int state) { + PipInput pipInput = mPipInputMap.get(inputId); + if (pipInput == null) { + // For tuner input, state change is handled in mChannelTunerListener. + return; + } + pipInput.updateAvailability(); + } + }; + + private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() { + @Override + public void onLoadFinished() { } + + @Override + public void onCurrentChannelUnavailable(Channel channel) { } + + @Override + public void onBrowsableChannelListChanged() { + PipInput tunerInput = mPipInputMap.get(TUNER_INPUT_ID); + if (tunerInput == null) { + return; + } + tunerInput.updateAvailability(); + } + + @Override + public void onChannelChanged(Channel previousChannel, Channel currentChannel) { + if (previousChannel != null && currentChannel != null + && !previousChannel.isPassthrough() && !currentChannel.isPassthrough()) { + // Channel change between channels for tuner inputs. + return; + } + PipInput previousMainInput = getPipInput(previousChannel); + if (previousMainInput != null) { + previousMainInput.updateAvailability(); + } + PipInput currentMainInput = getPipInput(currentChannel); + if (currentMainInput != null) { + currentMainInput.updateAvailability(); + } + } + }; + + public PipInputManager(Context context, TvInputManagerHelper inputManager, + ChannelTuner channelTuner) { + mContext = context; + mInputManager = inputManager; + mChannelTuner = channelTuner; + } + + /** + * Starts {@link PipInputManager}. + */ + public void start() { + if (mStarted) { + return; + } + mStarted = true; + mInputManager.addCallback(mTvInputCallback); + mChannelTuner.addListener(mChannelTunerListener); + initializePipInputList(); + } + + /** + * Stops {@link PipInputManager}. + */ + public void stop() { + if (!mStarted) { + return; + } + mStarted = false; + mInputManager.removeCallback(mTvInputCallback); + mChannelTuner.removeListener(mChannelTunerListener); + mPipInputMap.clear(); + } + + /** + * Adds a {@link PipInputManager.Listener}. + */ + public void addListener(Listener listener) { + mListeners.add(listener); + } + + /** + * Removes a {@link PipInputManager.Listener}. + */ + public void removeListener(Listener listener) { + mListeners.remove(listener); + } + + /** + * Gets the size of inputs for PIP. + * + * <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) { + 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; + } + + /** + * 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 = 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())); + } + } + } + if (!removeInputs.isEmpty()) { + pipInputs.removeAll(removeInputs); + } + Collections.sort(pipInputs, new Comparator<PipInput>() { + @Override + public int compare(PipInput lhs, PipInput rhs) { + if (!lhs.mIsPassthrough) { + return -1; + } + if (!rhs.mIsPassthrough) { + return 1; + } + String a = lhs.getLabel(); + String b = rhs.getLabel(); + return a.compareTo(b); + } + }); + return pipInputs; + } + + /** + * Returns an PIP input corresponding to {@code channel}. + */ + public PipInput getPipInput(Channel channel) { + if (channel == null) { + return null; + } + if (channel.isPassthrough()) { + return mPipInputMap.get(channel.getInputId()); + } else { + return mPipInputMap.get(TUNER_INPUT_ID); + } + } + + /** + * Returns true, if {@code channel1} and {@code channel2} belong to the same input. For example, + * two channels from different tuner inputs are also in the same input "Tuner" from PIP + * point of view. + */ + public boolean areInSamePipInput(Channel channel1, Channel channel2) { + PipInput input1 = getPipInput(channel1); + PipInput input2 = getPipInput(channel2); + return input1 != null && input2 != null + && getPipInput(channel1).equals(getPipInput(channel2)); + } + + private void initializePipInputList() { + boolean hasTunerInput = false; + for (TvInputInfo input : mInputManager.getTvInputInfos(false, false)) { + if (input.isPassthroughInput()) { + boolean available = mInputManager.getInputState(input) + == TvInputManager.INPUT_STATE_CONNECTED; + mPipInputMap.put(input.getId(), new PipInput(input.getId(), available)); + } else if (!hasTunerInput) { + hasTunerInput = true; + boolean available = mChannelTuner.getBrowsableChannelCount() != 0; + mPipInputMap.put(TUNER_INPUT_ID, new PipInput(TUNER_INPUT_ID, available)); + } + } + PipInput input = getPipInput(mChannelTuner.getCurrentChannel()); + if (input != null) { + input.updateAvailability(); + } + for (Listener l : mListeners) { + l.onPipInputListUpdated(); + } + } + + /** + * Listeners to notify PIP input state changes. + */ + public interface Listener { + /** + * Called when the state (availability) of PIP inputs is changed. + */ + void onPipInputStateUpdated(); + + /** + * Called when the list of PIP inputs is changed. + */ + void onPipInputListUpdated(); + } + + /** + * Input class for PIP. It has useful methods for PIP handling. + */ + public class PipInput { + private final String mInputId; + private final boolean mIsPassthrough; + private final TvInputInfo mInputInfo; + private boolean mAvailable; + + private PipInput(String inputId, boolean available) { + mInputId = inputId; + mIsPassthrough = !mInputId.equals(TUNER_INPUT_ID); + if (mIsPassthrough) { + mInputInfo = mInputManager.getTvInputInfo(mInputId); + } else { + mInputInfo = null; + } + mAvailable = available; + } + + /** + * Returns the {@link TvInputInfo} object that matches to this PIP input. + */ + public TvInputInfo getInputInfo() { + return mInputInfo; + } + + /** + * 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; + } + + /** + * Returns true, if the input is a passthrough TV input. + */ + public boolean isPassthrough() { + return mIsPassthrough; + } + + /** + * Gets a channel to play in a PIP view. + */ + public Channel getChannel() { + if (mIsPassthrough) { + return Channel.createPassthroughChannel(mInputId); + } else { + return mChannelTuner.findNearestBrowsableChannel( + Utils.getLastWatchedChannelId(mContext)); + } + } + + /** + * Gets a label of the input. + */ + public String getLabel() { + if (mIsPassthrough) { + return mInputInfo.loadLabel(mContext).toString(); + } else { + return mContext.getString(R.string.input_selector_tuner_label); + } + } + + /** + * Gets a long label including a customized label. + */ + public String getLongLabel() { + if (mIsPassthrough) { + String customizedLabel = Utils.loadLabel(mContext, mInputInfo); + String label = getLabel(); + if (label.equals(customizedLabel)) { + return customizedLabel; + } + return customizedLabel + " (" + label + ")"; + } else { + return mContext.getString(R.string.input_long_label_for_tuner); + } + } + + /** + * Updates availability. It returns true, if availability is changed. + */ + private void updateAvailability() { + boolean available; + // current playing input cannot be available for PIP. + Channel currentChannel = mChannelTuner.getCurrentChannel(); + if (mIsPassthrough) { + if (currentChannel != null && currentChannel.getInputId().equals(mInputId)) { + available = false; + } else { + available = mInputManager.getInputState(mInputId) + == TvInputManager.INPUT_STATE_CONNECTED; + } + } else { + if (currentChannel != null && !currentChannel.isPassthrough()) { + available = false; + } else { + available = mChannelTuner.getBrowsableChannelCount() > 0; + } + } + if (mAvailable != available) { + mAvailable = available; + for (Listener l : mListeners) { + l.onPipInputStateUpdated(); + } + } + } + + 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 324afe73..4135bd4e 100644 --- a/src/com/android/tv/util/RecurringRunner.java +++ b/src/com/android/tv/util/RecurringRunner.java @@ -57,15 +57,12 @@ public final class RecurringRunner { mHandler = new Handler(mContext.getMainLooper()); } - public void start(boolean restNextRunTime) { + public void start() { SoftPreconditions.checkState(!mRunning, TAG, mName + " start is called twice."); if (mRunning) { return; } mRunning = true; - if (restNextRunTime) { - resetNextRunTime(); - } new AsyncTask<Void, Void, Long>() { @Override protected Long doInBackground(Void... params) { @@ -79,10 +76,6 @@ public final class RecurringRunner { }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - public void start() { - start(false); - } - public void stop() { mRunning = false; mHandler.removeCallbacksAndMessages(null); diff --git a/src/com/android/tv/util/SearchManagerHelper.java b/src/com/android/tv/util/SearchManagerHelper.java new file mode 100644 index 00000000..b6e34d7a --- /dev/null +++ b/src/com/android/tv/util/SearchManagerHelper.java @@ -0,0 +1,61 @@ +/* + * 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.app.SearchManager; +import android.content.Context; +import android.os.Bundle; +import android.os.UserHandle; +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; + +/** + * A convenience class for calling methods in android.app.SearchManager. + */ +public final class SearchManagerHelper { + private static final String TAG = "SearchManagerHelper"; + + private static final Object sLock = new Object(); + private static SearchManagerHelper sInstance; + + private final SearchManager mSearchManager; + + private SearchManagerHelper(Context context) { + mSearchManager = ((android.app.SearchManager) context.getSystemService( + Context.SEARCH_SERVICE)); + } + + public static SearchManagerHelper getInstance(Context context) { + synchronized (sLock) { + if (sInstance == null) { + sInstance = new SearchManagerHelper(context.getApplicationContext()); + } + return sInstance; + } + } + + public void launchAssistAction() { + try { + SearchManager.class.getDeclaredMethod("launchLegacyAssist", String.class, Integer.TYPE, + Bundle.class).invoke(mSearchManager, null, UserHandle.myUserId(), null); + } catch (NoSuchMethodException | IllegalArgumentException | IllegalAccessException + | InvocationTargetException e) { + Log.e(TAG, "Fail to call SearchManager.launchAssistAction", e); + } + } +} diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java index 0dabe7c0..8223a81c 100644 --- a/src/com/android/tv/util/SetupUtils.java +++ b/src/com/android/tv/util/SetupUtils.java @@ -114,7 +114,7 @@ public class SetupUtils { @Override public void onLoadFinished() { manager.removeListener(this); - updateChannelsAfterSetup(mTvApplication, inputId, postRunnable); + updateChannelBrowsable(mTvApplication, inputId, postRunnable); } @Override @@ -124,18 +124,17 @@ public class SetupUtils { public void onChannelBrowsableChanged() { } }); } else { - updateChannelsAfterSetup(mTvApplication, inputId, postRunnable); + updateChannelBrowsable(mTvApplication, inputId, postRunnable); } } - private static void updateChannelsAfterSetup(Context context, final String inputId, + private static void updateChannelBrowsable(Context context, final String inputId, final Runnable postRunnable) { ApplicationSingletons appSingletons = TvApplication.getSingletons(context); final ChannelDataManager manager = appSingletons.getChannelDataManager(); manager.updateChannels(new Runnable() { @Override public void run() { - Channel firstChannelForInput = null; boolean browsableChanged = false; for (Channel channel : manager.getChannelList()) { if (channel.getInputId().equals(inputId)) { @@ -143,14 +142,8 @@ public class SetupUtils { manager.updateBrowsable(channel.getId(), true, true); browsableChanged = true; } - if (firstChannelForInput == null) { - firstChannelForInput = channel; - } } } - if (firstChannelForInput != null) { - Utils.setLastWatchedChannel(context, firstChannelForInput); - } if (browsableChanged) { manager.notifyChannelBrowsableChanged(); manager.applyUpdatedValuesToDb(); @@ -392,7 +385,10 @@ public class SetupUtils { // Start fetching program guide data for internal tuners. Context context = mTvApplication.getApplicationContext(); if (Utils.isInternalTvInput(context, inputId)) { - EpgFetcher.getInstance(context).startImmediately(true); + if (context.checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) + == PackageManager.PERMISSION_GRANTED && Experiments.CLOUD_EPG.get()) { + EpgFetcher.getInstance(context).startImmediately(); + } } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java index 67970ed6..121f56ed 100644 --- a/src/com/android/tv/util/TvInputManagerHelper.java +++ b/src/com/android/tv/util/TvInputManagerHelper.java @@ -18,26 +18,20 @@ package com.android.tv.util; import android.content.Context; import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; -import android.hardware.hdmi.HdmiDeviceInfo; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvInputManager.TvInputCallback; import android.os.Handler; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; -import android.util.ArrayMap; import android.util.Log; import com.android.tv.Features; import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.TvCommonUtils; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.parental.ParentalControlSettings; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -48,64 +42,14 @@ import java.util.Map; public class TvInputManagerHelper { private static final String TAG = "TvInputManagerHelper"; private static final boolean DEBUG = false; - - /** - * Types of HDMI device and bundled tuner. - */ - public static final int TYPE_CEC_DEVICE = -2; - public static final int TYPE_BUNDLED_TUNER = -3; - public static final int TYPE_CEC_DEVICE_RECORDER = -4; - public static final int TYPE_CEC_DEVICE_PLAYBACK = -5; - public static final int TYPE_MHL_MOBILE = -6; - - private static final String PERMISSION_ACCESS_ALL_EPG_DATA = - "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA"; - private static final String [] mPhysicalTunerBlackList = { - }; - private static final String META_LABEL_SORT_KEY = "input_sort_key"; - - /** - * The default tv input priority to show. - */ - private static final ArrayList<Integer> DEFAULT_TV_INPUT_PRIORITY = new ArrayList<>(); - static { - DEFAULT_TV_INPUT_PRIORITY.add(TYPE_BUNDLED_TUNER); - DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_TUNER); - DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE); - DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE_RECORDER); - DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE_PLAYBACK); - DEFAULT_TV_INPUT_PRIORITY.add(TYPE_MHL_MOBILE); - DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_HDMI); - DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_DVI); - DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_COMPONENT); - DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_SVIDEO); - DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_COMPOSITE); - DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_DISPLAY_PORT); - DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_VGA); - DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_SCART); - DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_OTHER); - } - private static final String[] PARTNER_TUNER_INPUT_PREFIX_BLACKLIST = { }; - private static final String[] TESTABLE_INPUTS = { - "com.android.tv.testinput/.TestTvInputService" - }; - private final Context mContext; - private final PackageManager mPackageManager; private final TvInputManager mTvInputManager; private final Map<String, Integer> mInputStateMap = new HashMap<>(); private final Map<String, TvInputInfo> mInputMap = new HashMap<>(); - private final Map<String, String> mTvInputLabels = new ArrayMap<>(); - private final Map<String, String> mTvInputCustomLabels = new ArrayMap<>(); private final Map<String, Boolean> mInputIdToPartnerInputMap = new HashMap<>(); - - private final Map<String, CharSequence> mTvInputApplicationLabels = new ArrayMap<>(); - private final Map<String, Drawable> mTvInputApplicationIcons = new ArrayMap<>(); - private final Map<String, Drawable> mTvInputAppliactionBanners = new ArrayMap<>(); - private final TvInputCallback mInternalCallback = new TvInputCallback() { @Override public void onInputStateChanged(String inputId, int state) { @@ -128,11 +72,6 @@ public class TvInputManagerHelper { TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); if (info != null) { mInputMap.put(inputId, info); - mTvInputLabels.put(inputId, info.loadLabel(mContext).toString()); - CharSequence inputCustomLabel = info.loadCustomLabel(mContext); - if (inputCustomLabel != null) { - mTvInputCustomLabels.put(inputId, inputCustomLabel.toString()); - } mInputStateMap.put(inputId, mTvInputManager.getInputState(inputId)); mInputIdToPartnerInputMap.put(inputId, isPartnerInput(info)); } @@ -146,11 +85,6 @@ public class TvInputManagerHelper { public void onInputRemoved(String inputId) { if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId); mInputMap.remove(inputId); - mTvInputLabels.remove(inputId); - mTvInputCustomLabels.remove(inputId); - mTvInputApplicationLabels.remove(inputId); - mTvInputApplicationIcons.remove(inputId); - mTvInputAppliactionBanners.remove(inputId); mInputStateMap.remove(inputId); mInputIdToPartnerInputMap.remove(inputId); mContentRatingsManager.update(); @@ -169,14 +103,6 @@ public class TvInputManagerHelper { } TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); mInputMap.put(inputId, info); - mTvInputLabels.put(inputId, info.loadLabel(mContext).toString()); - CharSequence inputCustomLabel = info.loadCustomLabel(mContext); - if (inputCustomLabel != null) { - mTvInputCustomLabels.put(inputId, inputCustomLabel.toString()); - } - mTvInputApplicationLabels.remove(inputId); - mTvInputApplicationIcons.remove(inputId); - mTvInputAppliactionBanners.remove(inputId); for (TvInputCallback callback : mCallbacks) { callback.onInputUpdated(inputId); } @@ -188,11 +114,6 @@ public class TvInputManagerHelper { public void onTvInputInfoUpdated(TvInputInfo inputInfo) { if (DEBUG) Log.d(TAG, "onTvInputInfoUpdated " + inputInfo); mInputMap.put(inputInfo.getId(), inputInfo); - mTvInputLabels.put(inputInfo.getId(), inputInfo.loadLabel(mContext).toString()); - CharSequence inputCustomLabel = inputInfo.loadCustomLabel(mContext); - if (inputCustomLabel != null) { - mTvInputCustomLabels.put(inputInfo.getId(), inputCustomLabel.toString()); - } for (TvInputCallback callback : mCallbacks) { callback.onTvInputInfoUpdated(inputInfo); } @@ -210,7 +131,6 @@ public class TvInputManagerHelper { public TvInputManagerHelper(Context context) { mContext = context.getApplicationContext(); - mPackageManager = context.getPackageManager(); mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); mContentRatingsManager = new ContentRatingsManager(context); mParentalControlSettings = new ParentalControlSettings(context); @@ -225,11 +145,6 @@ public class TvInputManagerHelper { mStarted = true; mTvInputManager.registerCallback(mInternalCallback, mHandler); mInputMap.clear(); - mTvInputLabels.clear(); - mTvInputCustomLabels.clear(); - mTvInputApplicationLabels.clear(); - mTvInputApplicationIcons.clear(); - mTvInputAppliactionBanners.clear(); mInputStateMap.clear(); mInputIdToPartnerInputMap.clear(); for (TvInputInfo input : mTvInputManager.getTvInputList()) { @@ -256,23 +171,9 @@ public class TvInputManagerHelper { mStarted = false; mInputStateMap.clear(); mInputMap.clear(); - mTvInputLabels.clear(); - mTvInputCustomLabels.clear(); - mTvInputApplicationLabels.clear(); - mTvInputApplicationIcons.clear(); - mTvInputAppliactionBanners.clear();; mInputIdToPartnerInputMap.clear(); } - /** - * Clears the TvInput labels map. - */ - public void clearTvInputLabels() { - mTvInputLabels.clear(); - mTvInputCustomLabels.clear(); - mTvInputApplicationLabels.clear(); - } - public List<TvInputInfo> getTvInputInfos(boolean availableOnly, boolean tunerOnly) { ArrayList<TvInputInfo> list = new ArrayList<>(); for (Map.Entry<String, Integer> pair : mInputStateMap.entrySet()) { @@ -344,69 +245,7 @@ public class TvInputManagerHelper { */ @VisibleForTesting public String loadLabel(TvInputInfo info) { - String label = mTvInputLabels.get(info.getId()); - if (label == null) { - label = info.loadLabel(mContext).toString(); - mTvInputLabels.put(info.getId(), label); - } - return label; - } - - /** - * Loads custom label of {@code info} - */ - public String loadCustomLabel(TvInputInfo info) { - String customLabel = mTvInputCustomLabels.get(info.getId()); - if (customLabel == null) { - CharSequence customLabelCharSequence = info.loadCustomLabel(mContext); - if (customLabelCharSequence != null) { - customLabel = customLabelCharSequence.toString(); - mTvInputCustomLabels.put(info.getId(), customLabel); - } - } - return customLabel; - } - - /** - * Gets the tv input application's label. - */ - public CharSequence getTvInputApplicationLabel(CharSequence inputId) { - return mTvInputApplicationLabels.get(inputId); - } - - /** - * Stores the tv input application's label. - */ - public void setTvInputApplicationLabel(String inputId, CharSequence label) { - mTvInputApplicationLabels.put(inputId, label); - } - - /** - * Gets the tv input application's icon. - */ - public Drawable getTvInputApplicationIcon(String inputId) { - return mTvInputApplicationIcons.get(inputId); - } - - /** - * Stores the tv input application's icon. - */ - public void setTvInputApplicationIcon(String inputId, Drawable icon) { - mTvInputApplicationIcons.put(inputId, icon); - } - - /** - * Gets the tv input application's banner. - */ - public Drawable getTvInputApplicationBanner(String inputId) { - return mTvInputAppliactionBanners.get(inputId); - } - - /** - * Stores the tv input application's banner. - */ - public void setTvInputApplicationBanner(String inputId, Drawable banner) { - mTvInputAppliactionBanners.put(inputId, banner); + return info.loadLabel(mContext).toString(); } /** @@ -482,54 +321,14 @@ public class TvInputManagerHelper { return mContentRatingsManager; } - private int getInputSortKey(TvInputInfo input) { - return input.getServiceInfo().metaData.getInt(META_LABEL_SORT_KEY, - Integer.MAX_VALUE); - } - - private boolean isInputPhysicalTuner(TvInputInfo input) { - String packageName = input.getServiceInfo().packageName; - if (Arrays.asList(mPhysicalTunerBlackList).contains(packageName)) { - return false; - } - - if (input.createSetupIntent() == null) { - return false; - } else { - boolean mayBeTunerInput = mPackageManager.checkPermission( - PERMISSION_ACCESS_ALL_EPG_DATA, input.getServiceInfo().packageName) - == PackageManager.PERMISSION_GRANTED; - if (!mayBeTunerInput) { - try { - ApplicationInfo ai = mPackageManager.getApplicationInfo( - input.getServiceInfo().packageName, 0); - if ((ai.flags & (ApplicationInfo.FLAG_SYSTEM - | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) == 0) { - return false; - } - } catch (PackageManager.NameNotFoundException e) { - return false; - } - } - } - return true; - } - private boolean isInBlackList(String inputId) { - if (Features.USE_PARTNER_INPUT_BLACKLIST.isEnabled(mContext)) { - for (String disabledTunerInputPrefix : PARTNER_TUNER_INPUT_PREFIX_BLACKLIST) { - if (inputId.contains(disabledTunerInputPrefix)) { - return true; - } - } + if (!Features.USE_PARTNER_INPUT_BLACKLIST.isEnabled(mContext)) { + return false; } - if (TvCommonUtils.isRunningInTest()) { - for (String testableInput : TESTABLE_INPUTS) { - if (testableInput.equals(inputId)) { - return false; - } + for (String disabledTunerInputPrefix : PARTNER_TUNER_INPUT_PREFIX_BLACKLIST) { + if (inputId.contains(disabledTunerInputPrefix)) { + return true; } - return true; } return false; } @@ -558,123 +357,4 @@ public class TvInputManagerHelper { return mInputManager.loadLabel(lhs).compareTo(mInputManager.loadLabel(rhs)); } } - - /** - * A comparator used for {@link com.android.tv.ui.SelectInputView} to show the list of - * TV inputs. - */ - public static class InputComparator implements Comparator<TvInputInfo> { - private Map<Integer, Integer> mTypePriorities = new HashMap<>(); - private final TvInputManagerHelper mTvInputManagerHelper; - private final Context mContext; - - public InputComparator(Context context, TvInputManagerHelper tvInputManagerHelper) { - mContext = context; - mTvInputManagerHelper = tvInputManagerHelper; - setupDeviceTypePriorities(); - } - - @Override - public int compare(TvInputInfo lhs, TvInputInfo rhs) { - if (lhs == null) { - return (rhs == null) ? 0 : 1; - } - if (rhs == null) { - return -1; - } - - boolean enabledL = (mTvInputManagerHelper.getInputState(lhs) - != TvInputManager.INPUT_STATE_DISCONNECTED); - boolean enabledR = (mTvInputManagerHelper.getInputState(rhs) - != TvInputManager.INPUT_STATE_DISCONNECTED); - if (enabledL != enabledR) { - return enabledL ? -1 : 1; - } - - int priorityL = getPriority(lhs); - int priorityR = getPriority(rhs); - if (priorityL != priorityR) { - return priorityL - priorityR; - } - - if (lhs.getType() == TvInputInfo.TYPE_TUNER - && rhs.getType() == TvInputInfo.TYPE_TUNER) { - boolean isPhysicalL = mTvInputManagerHelper.isInputPhysicalTuner(lhs); - boolean isPhysicalR = mTvInputManagerHelper.isInputPhysicalTuner(rhs); - if (isPhysicalL != isPhysicalR) { - return isPhysicalL ? -1 : 1; - } - } - - int sortKeyL = mTvInputManagerHelper.getInputSortKey(lhs); - int sortKeyR = mTvInputManagerHelper.getInputSortKey(rhs); - if (sortKeyL != sortKeyR) { - return sortKeyR - sortKeyL; - } - - String parentLabelL = lhs.getParentId() != null - ? getLabel(mTvInputManagerHelper.getTvInputInfo(lhs.getParentId())) - : getLabel(mTvInputManagerHelper.getTvInputInfo(lhs.getId())); - String parentLabelR = rhs.getParentId() != null - ? getLabel(mTvInputManagerHelper.getTvInputInfo(rhs.getParentId())) - : getLabel(mTvInputManagerHelper.getTvInputInfo(rhs.getId())); - - if (!TextUtils.equals(parentLabelL, parentLabelR)) { - return parentLabelL.compareToIgnoreCase(parentLabelR); - } - return getLabel(lhs).compareToIgnoreCase(getLabel(rhs)); - } - - private String getLabel(TvInputInfo input) { - if (input == null) { - return ""; - } - String label = mTvInputManagerHelper.loadCustomLabel(input); - if (TextUtils.isEmpty(label)) { - label = mTvInputManagerHelper.loadLabel(input); - } - return label; - } - - private int getPriority(TvInputInfo info) { - Integer priority = null; - if (mTypePriorities != null) { - priority = mTypePriorities.get(getTvInputTypeForPriority(info)); - } - if (priority != null) { - return priority; - } - return Integer.MAX_VALUE; - } - - private void setupDeviceTypePriorities() { - mTypePriorities = Partner.getInstance(mContext).getInputsOrderMap(); - - // Fill in any missing priorities in the map we got from the OEM - int priority = mTypePriorities.size(); - for (int type : DEFAULT_TV_INPUT_PRIORITY) { - if (!mTypePriorities.containsKey(type)) { - mTypePriorities.put(type, priority++); - } - } - } - - private int getTvInputTypeForPriority(TvInputInfo info) { - if (info.getHdmiDeviceInfo() != null) { - if (info.getHdmiDeviceInfo().isCecDevice()) { - switch (info.getHdmiDeviceInfo().getDeviceType()) { - case HdmiDeviceInfo.DEVICE_RECORDER: - return TYPE_CEC_DEVICE_RECORDER; - case HdmiDeviceInfo.DEVICE_PLAYBACK: - return TYPE_CEC_DEVICE_PLAYBACK; - default: - return TYPE_CEC_DEVICE; - } - } else if (info.getHdmiDeviceInfo().isMhlDevice()) { - return TYPE_MHL_MOBILE; - } - } - return info.getType(); - } - } } diff --git a/src/com/android/tv/util/TvSettings.java b/src/com/android/tv/util/TvSettings.java index 970cd055..97ff59d6 100644 --- a/src/com/android/tv/util/TvSettings.java +++ b/src/com/android/tv/util/TvSettings.java @@ -17,8 +17,6 @@ package com.android.tv.util; import android.content.Context; -import android.content.SharedPreferences; -import android.media.tv.TvTrackInfo; import android.preference.PreferenceManager; import android.support.annotation.IntDef; @@ -28,6 +26,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; + /** * A class about the constants for TV settings. * Objects that are returned from the various {@code get} methods must be treated as immutable. @@ -36,21 +35,44 @@ public final class TvSettings { private TvSettings() {} public static final String PREF_DISPLAY_MODE = "display_mode"; // int value + public static final String PREF_PIP_LAYOUT = "pip_layout"; // int value + public static final String PREF_PIP_SIZE = "pip_size"; // int value public static final String PREF_PIN = "pin"; // 4-digit string value. Otherwise, it's not set. + // PIP sounds + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + PIP_SOUND_MAIN, PIP_SOUND_PIP_WINDOW }) + public @interface PipSound {} + public static final int PIP_SOUND_MAIN = 0; + public static final int PIP_SOUND_PIP_WINDOW = PIP_SOUND_MAIN + 1; + + // PIP layouts + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + PIP_LAYOUT_BOTTOM_RIGHT, PIP_LAYOUT_TOP_RIGHT, PIP_LAYOUT_TOP_LEFT, + PIP_LAYOUT_BOTTOM_LEFT, PIP_LAYOUT_SIDE_BY_SIDE }) + public @interface PipLayout {} + public static final int PIP_LAYOUT_BOTTOM_RIGHT = 0; + public static final int PIP_LAYOUT_TOP_RIGHT = PIP_LAYOUT_BOTTOM_RIGHT + 1; + public static final int PIP_LAYOUT_TOP_LEFT = PIP_LAYOUT_TOP_RIGHT + 1; + public static final int PIP_LAYOUT_BOTTOM_LEFT = PIP_LAYOUT_TOP_LEFT + 1; + public static final int PIP_LAYOUT_SIDE_BY_SIDE = PIP_LAYOUT_BOTTOM_LEFT + 1; + public static final int PIP_LAYOUT_LAST = PIP_LAYOUT_SIDE_BY_SIDE; + + // PIP sizes + @Retention(RetentionPolicy.SOURCE) + @IntDef({ PIP_SIZE_SMALL, PIP_SIZE_BIG }) + public @interface PipSize {} + public static final int PIP_SIZE_SMALL = 0; + public static final int PIP_SIZE_BIG = PIP_SIZE_SMALL + 1; + public static final int PIP_SIZE_LAST = PIP_SIZE_BIG; + // Multi-track audio settings private static final String PREF_MULTI_AUDIO_ID = "pref.multi_audio_id"; private static final String PREF_MULTI_AUDIO_LANGUAGE = "pref.multi_audio_language"; private static final String PREF_MULTI_AUDIO_CHANNEL_COUNT = "pref.multi_audio_channel_count"; - // DVR Multi-audio and subtitle settings - private static final String PREF_DVR_MULTI_AUDIO_ID = "pref.dvr_multi_audio_id"; - private static final String PREF_DVR_MULTI_AUDIO_LANGUAGE = "pref.dvr_multi_audio_language"; - private static final String PREF_DVR_MULTI_AUDIO_CHANNEL_COUNT = - "pref.dvr_multi_audio_channel_count"; - private static final String PREF_DVR_SUBTITLE_ID = "pref.dvr_subtitle_id"; - private static final String PREF_DVR_SUBTITLE_LANGUAGE = "pref.dvr_subtitle_language"; - // Parental Control settings private static final String PREF_CONTENT_RATING_SYSTEMS = "pref.content_rating_systems"; private static final String PREF_CONTENT_RATING_LEVEL = "pref.content_rating_level"; @@ -67,6 +89,59 @@ public final class TvSettings { public static final int CONTENT_RATING_LEVEL_LOW = 3; public static final int CONTENT_RATING_LEVEL_CUSTOM = 4; + // PIP settings + /** + * Returns the layout of the PIP window stored in the shared preferences. + * + * @return the saved layout of the PIP window. This value is one of + * {@link #PIP_LAYOUT_TOP_LEFT}, {@link #PIP_LAYOUT_TOP_RIGHT}, + * {@link #PIP_LAYOUT_BOTTOM_LEFT}, {@link #PIP_LAYOUT_BOTTOM_RIGHT} and + * {@link #PIP_LAYOUT_SIDE_BY_SIDE}. If the preference value does not exist, + * {@link #PIP_LAYOUT_BOTTOM_RIGHT} is returned. + */ + @SuppressWarnings("ResourceType") + @PipLayout + public static int getPipLayout(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getInt( + PREF_PIP_LAYOUT, PIP_LAYOUT_BOTTOM_RIGHT); + } + + /** + * Stores the layout of PIP window to the shared preferences. + * + * @param pipLayout This value should be one of {@link #PIP_LAYOUT_TOP_LEFT}, + * {@link #PIP_LAYOUT_TOP_RIGHT}, {@link #PIP_LAYOUT_BOTTOM_LEFT}, + * {@link #PIP_LAYOUT_BOTTOM_RIGHT} and {@link #PIP_LAYOUT_SIDE_BY_SIDE}. + */ + public static void setPipLayout(Context context, @PipLayout int pipLayout) { + PreferenceManager.getDefaultSharedPreferences(context).edit().putInt( + PREF_PIP_LAYOUT, pipLayout).apply(); + } + + /** + * Returns the size of the PIP view stored in the shared preferences. + * + * @return the saved size of the PIP view. This value is one of + * {@link #PIP_SIZE_SMALL} and {@link #PIP_SIZE_BIG}. If the preference value does not + * exist, {@link #PIP_SIZE_SMALL} is returned. + */ + @SuppressWarnings("ResourceType") + @PipSize + public static int getPipSize(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getInt( + PREF_PIP_SIZE, PIP_SIZE_SMALL); + } + + /** + * Stores the size of PIP view to the shared preferences. + * + * @param pipSize This value should be one of {@link #PIP_SIZE_SMALL} and {@link #PIP_SIZE_BIG}. + */ + public static void setPipSize(Context context, @PipSize int pipSize) { + PreferenceManager.getDefaultSharedPreferences(context).edit().putInt( + PREF_PIP_SIZE, pipSize).apply(); + } + // Multi-track audio settings public static String getMultiAudioId(Context context) { return PreferenceManager.getDefaultSharedPreferences(context).getString( @@ -98,57 +173,6 @@ public final class TvSettings { PREF_MULTI_AUDIO_CHANNEL_COUNT, channelCount).apply(); } - public static void setDvrPlaybackTrackSettings(Context context, int trackType, - TvTrackInfo info) { - if (trackType == TvTrackInfo.TYPE_AUDIO) { - if (info == null) { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .remove(PREF_DVR_MULTI_AUDIO_ID).apply(); - } else { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putString(PREF_DVR_MULTI_AUDIO_LANGUAGE, info.getLanguage()) - .putInt(PREF_DVR_MULTI_AUDIO_CHANNEL_COUNT, info.getAudioChannelCount()) - .putString(PREF_DVR_MULTI_AUDIO_ID, info.getId()).apply(); - } - } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { - if (info == null) { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .remove(PREF_DVR_SUBTITLE_ID).apply(); - } else { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putString(PREF_DVR_SUBTITLE_LANGUAGE, info.getLanguage()) - .putString(PREF_DVR_SUBTITLE_ID, info.getId()).apply(); - } - } - } - - public static TvTrackInfo getDvrPlaybackTrackSettings(Context context, - int trackType) { - String language; - String trackId; - int channelCount; - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); - if (trackType == TvTrackInfo.TYPE_AUDIO) { - trackId = pref.getString(PREF_DVR_MULTI_AUDIO_ID, null); - if (trackId == null) { - return null; - } - language = pref.getString(PREF_DVR_MULTI_AUDIO_LANGUAGE, null); - channelCount = pref.getInt(PREF_DVR_MULTI_AUDIO_CHANNEL_COUNT, 0); - return new TvTrackInfo.Builder(trackType, trackId) - .setLanguage(language).setAudioChannelCount(channelCount).build(); - } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { - trackId = pref.getString(PREF_DVR_SUBTITLE_ID, null); - if (trackId == null) { - return null; - } - language = pref.getString(PREF_DVR_SUBTITLE_LANGUAGE, null); - return new TvTrackInfo.Builder(trackType, trackId).setLanguage(language).build(); - } else { - return null; - } - } - // Parental Control settings public static void addContentRatingSystems(Context context, Set<String> ids) { Set<String> contentRatingSystemSet = getContentRatingSystemSet(context); @@ -230,4 +254,4 @@ public final class TvSettings { PreferenceManager.getDefaultSharedPreferences(context).edit().putLong( PREF_DISABLE_PIN_UNTIL, timeMillis).apply(); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/util/TvTrackInfoUtils.java b/src/com/android/tv/util/TvTrackInfoUtils.java index 667cc9bf..c004f001 100644 --- a/src/com/android/tv/util/TvTrackInfoUtils.java +++ b/src/com/android/tv/util/TvTrackInfoUtils.java @@ -52,22 +52,35 @@ public class TvTrackInfoUtils { } // Assumes {@code null} language matches to any language since it means user hasn't // selected any track before or selected a track without language information. - boolean lhsLangMatch = language == null || Utils.isEqualLanguage(lhs.getLanguage(), - language); boolean rhsLangMatch = language == null || Utils.isEqualLanguage(rhs.getLanguage(), language); - if (lhsLangMatch && rhsLangMatch) { - boolean lhsCountMatch = lhs.getType() != TvTrackInfo.TYPE_AUDIO - || lhs.getAudioChannelCount() == channelCount; - boolean rhsCountMatch = rhs.getType() != TvTrackInfo.TYPE_AUDIO - || rhs.getAudioChannelCount() == channelCount; - if (lhsCountMatch && rhsCountMatch) { - return Boolean.compare(lhs.getId().equals(id), rhs.getId().equals(id)); + boolean lhsLangMatch = language == null || Utils.isEqualLanguage(lhs.getLanguage(), + language); + if (rhsLangMatch) { + if (lhsLangMatch) { + boolean rhsCountMatch = rhs.getAudioChannelCount() == channelCount; + boolean lhsCountMatch = lhs.getAudioChannelCount() == channelCount; + if (rhsCountMatch) { + if (lhsCountMatch) { + boolean rhsIdMatch = rhs.getId().equals(id); + boolean lhsIdMatch = lhs.getId().equals(id); + if (rhsIdMatch) { + return lhsIdMatch ? 0 : -1; + } else { + return lhsIdMatch ? 1 : 0; + } + + } else { + return -1; + } + } else { + return lhsCountMatch ? 1 : 0; + } } else { - return Boolean.compare(lhsCountMatch, rhsCountMatch); + return -1; } } else { - return Boolean.compare(lhsLangMatch, rhsLangMatch); + return lhsLangMatch ? 1 : 0; } } }; @@ -99,4 +112,4 @@ public class TvTrackInfoUtils { private TvTrackInfoUtils() { } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java index 3fe2ec3d..99d34431 100644 --- a/src/com/android/tv/util/Utils.java +++ b/src/com/android/tv/util/Utils.java @@ -44,13 +44,11 @@ import android.view.View; import com.android.tv.ApplicationSingletons; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.common.BuildConfig; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.GenreItems; import com.android.tv.data.Program; import com.android.tv.data.StreamInfo; -import com.android.tv.experiments.Experiments; import java.io.File; import java.text.SimpleDateFormat; @@ -85,6 +83,9 @@ public class Utils { public static final String EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED = "recorded_program_pin_checked"; + // Query parameter in the intent of starting MainActivity. + public static final String PARAM_SOURCE = "source"; + private static final String PATH_CHANNEL = "channel"; private static final String PATH_PROGRAM = "program"; @@ -96,8 +97,6 @@ public class Utils { "last_watched_tuner_input_id"; private static final String PREF_KEY_RECORDING_FAILED_REASONS = "recording_failed_reasons"; - private static final String PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET = - "failed_scheduled_recording_info_set"; private static final int VIDEO_SD_WIDTH = 704; private static final int VIDEO_SD_HEIGHT = 480; @@ -115,7 +114,6 @@ public class Utils { private static final int AUDIO_CHANNEL_SURROUND_8 = 8; private static final long RECORDING_FAILED_REASON_NONE = 0; - private static final long HALF_MINUTE_MS = TimeUnit.SECONDS.toMillis(30); private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); // Hardcoded list for known bundled inputs not written by OEM/SOCs. @@ -209,28 +207,6 @@ public class Utils { } /** - * Adds the info of failed scheduled recording. - */ - public static void addFailedScheduledRecordingInfo(Context context, - String scheduledRecordingInfo) { - Set<String> failedScheduledRecordingInfoSet = getFailedScheduledRecordingInfoSet(context); - failedScheduledRecordingInfoSet.add(scheduledRecordingInfo); - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putStringSet(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET, - failedScheduledRecordingInfoSet) - .apply(); - } - - /** - * Clears the failed scheduled recording info set. - */ - public static void clearFailedScheduledRecordingInfoSet(Context context) { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .remove(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET) - .apply(); - } - - /** * Clears recording failed reason. */ public static void clearRecordingFailedReason(Context context, int reason) { @@ -270,14 +246,6 @@ public class Utils { } /** - * Returns the failed scheduled recordings info set. - */ - public static Set<String> getFailedScheduledRecordingInfoSet(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context) - .getStringSet(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET, new HashSet<>()); - } - - /** * Checks do recording failed reason exist. */ public static boolean hasRecordingFailedReason(Context context, int reason) { @@ -365,14 +333,6 @@ public class Utils { } /** - * Returns the round off minutes when convert milliseconds to minutes. - */ - public static int getRoundOffMinsFromMs(long millis) { - // Round off the result by adding half minute to the original ms. - return (int) TimeUnit.MILLISECONDS.toMinutes(millis + HALF_MINUTE_MS); - } - - /** * Returns duration string according to the date & time format. * If {@code startUtcMillis} and {@code endUtcMills} are equal, * formatted time will be returned instead. @@ -432,18 +392,16 @@ public class Utils { : DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flag); } - /** - * Checks if two given time (in milliseconds) are in the same day with regard to the - * locale timezone. - */ + @VisibleForTesting public static boolean isInGivenDay(long dayToMatchInMillis, long subjectTimeInMillis) { + final long DAY_IN_MS = TimeUnit.DAYS.toMillis(1); TimeZone timeZone = Calendar.getInstance().getTimeZone(); long offset = timeZone.getRawOffset(); if (timeZone.inDaylightTime(new Date(dayToMatchInMillis))) { offset += timeZone.getDSTSavings(); } - return Utils.floorTime(dayToMatchInMillis + offset, ONE_DAY_MS) - == Utils.floorTime(subjectTimeInMillis + offset, ONE_DAY_MS); + return Utils.floorTime(dayToMatchInMillis + offset, DAY_IN_MS) + == Utils.floorTime(subjectTimeInMillis + offset, DAY_IN_MS); } /** @@ -565,7 +523,7 @@ public class Utils { if (track.getType() != TvTrackInfo.TYPE_AUDIO) { throw new IllegalArgumentException("Not an audio track: " + track); } - String language = context.getString(R.string.multi_audio_unknown_language); + String language = context.getString(R.string.default_language); if (!TextUtils.isEmpty(track.getLanguage())) { language = new Locale(track.getLanguage()).getDisplayName(); } else { @@ -902,11 +860,4 @@ public class Utils { } return Genres.encode(genres); } - - /** - * Returns true if the current user is a developer. - */ - public static boolean isDeveloper() { - return BuildConfig.ENG || Experiments.ENABLE_DEVELOPER_FEATURES.get(); - } } diff --git a/src/com/android/tv/util/ViewCache.java b/src/com/android/tv/util/ViewCache.java deleted file mode 100644 index 113bda27..00000000 --- a/src/com/android/tv/util/ViewCache.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.android.tv.util; - -import android.util.SparseArray; -import android.view.View; - -import java.util.ArrayList; - -/** - * A cache for the views. - */ -public class ViewCache { - private final static SparseArray<ArrayList<View>> mViews = new SparseArray(); - - private static ViewCache sViewCache; - - private ViewCache() { } - - /** - * Returns an instance of the view cache. - */ - public static ViewCache getInstance() { - if (sViewCache == null) { - return new ViewCache(); - } else { - return sViewCache; - } - } - - /** - * Returns if the view cache is empty. - */ - public boolean isEmpty() { - return mViews.size() == 0; - } - - /** - * Stores a view into this view cache. - */ - public void putView(int resId, View view) { - ArrayList<View> views = mViews.get(resId); - if (views == null) { - views = new ArrayList(); - mViews.put(resId, views); - } - views.add(view); - } - - /** - * Returns the view for specific resource id. - */ - public View getView(int resId) { - ArrayList<View> views = mViews.get(resId); - if (views != null && !views.isEmpty()) { - View view = views.remove(views.size() - 1); - if (views.isEmpty()) { - mViews.remove(resId); - } - return view; - } else { - return null; - } - } - - /** - * Clears the view cache. - */ - public void clear() { - mViews.clear(); - } -} |