From 07b043dc3db83d6d20f0e8513b946830ab00e37b Mon Sep 17 00:00:00 2001 From: Nick Chalko Date: Tue, 1 Sep 2015 09:05:04 -0700 Subject: Sync to ub-tv-friends at 1.06.202 git hash 3c1965f5dcc60243f1fe600cb35f19bd5f01fc27 Change-Id: I90b77790f9074677ecef72a23235d2b33eacb76a --- src/com/android/tv/ChannelTuner.java | 2 + src/com/android/tv/LauncherActivity.java | 4 +- src/com/android/tv/MainActivity.java | 221 +++--- src/com/android/tv/TimeShiftManager.java | 49 +- src/com/android/tv/TvApplication.java | 71 +- src/com/android/tv/analytics/HasTrackerLabel.java | 2 +- src/com/android/tv/analytics/StubTracker.java | 16 +- src/com/android/tv/analytics/Tracker.java | 42 +- src/com/android/tv/customization/CustomAction.java | 9 - src/com/android/tv/data/Channel.java | 195 ++--- src/com/android/tv/data/ChannelDataManager.java | 56 +- src/com/android/tv/data/ChannelLogoFetcher.java | 22 +- src/com/android/tv/data/DisplayMode.java | 5 + src/com/android/tv/data/Program.java | 104 +-- src/com/android/tv/data/ProgramDataManager.java | 17 +- src/com/android/tv/data/StreamInfo.java | 2 - src/com/android/tv/dialog/PinDialogFragment.java | 4 +- .../tv/dialog/SafeDismissDialogFragment.java | 1 - src/com/android/tv/guide/ProgramGrid.java | 36 + src/com/android/tv/guide/ProgramGuide.java | 98 ++- src/com/android/tv/guide/ProgramItemView.java | 12 +- src/com/android/tv/guide/ProgramManager.java | 21 +- src/com/android/tv/menu/AppLinkCardView.java | 4 +- .../android/tv/menu/ChannelsPosterPrefetcher.java | 52 +- src/com/android/tv/menu/ChannelsRow.java | 24 +- src/com/android/tv/menu/ChannelsRowAdapter.java | 4 +- src/com/android/tv/menu/IMenuView.java | 60 ++ src/com/android/tv/menu/ItemListRow.java | 8 +- src/com/android/tv/menu/Menu.java | 330 +++++++++ src/com/android/tv/menu/MenuLayoutManager.java | 819 +++++++++++++++++++++ src/com/android/tv/menu/MenuRow.java | 34 +- src/com/android/tv/menu/MenuRowFactory.java | 118 +++ src/com/android/tv/menu/MenuRowView.java | 237 ++---- src/com/android/tv/menu/MenuView.java | 691 ++++------------- src/com/android/tv/menu/PlayControlsRow.java | 20 +- src/com/android/tv/menu/PlayControlsRowView.java | 50 +- src/com/android/tv/menu/TvOptionsRowAdapter.java | 4 +- .../tv/receiver/AudioCapabilitiesReceiver.java | 29 +- .../tv/recommendation/NotificationService.java | 140 ++-- .../recommendation/RecommendationDataManager.java | 129 ++-- .../tv/recommendation/RoutineWatchEvaluator.java | 2 - src/com/android/tv/search/TvProviderSearch.java | 13 +- src/com/android/tv/ui/ChannelBannerView.java | 6 +- src/com/android/tv/ui/FullscreenDialogView.java | 106 +-- src/com/android/tv/ui/InputBannerView.java | 6 +- src/com/android/tv/ui/IntroView.java | 36 +- src/com/android/tv/ui/KeypadChannelSwitchView.java | 6 +- .../tv/ui/OnRepeatedKeyInterceptListener.java | 112 +++ src/com/android/tv/ui/SelectInputView.java | 6 +- src/com/android/tv/ui/SetupView.java | 178 ++++- src/com/android/tv/ui/TunableTvView.java | 7 +- src/com/android/tv/ui/TvOverlayManager.java | 206 ++++-- src/com/android/tv/ui/TvViewUiManager.java | 20 +- .../tv/ui/sidepanel/ChannelSourcesFragment.java | 9 +- .../ui/sidepanel/CustomizeChannelListFragment.java | 42 +- .../parentalcontrols/ChannelsBlockedFragment.java | 42 +- src/com/android/tv/util/AsyncDbTask.java | 5 + src/com/android/tv/util/BitmapUtils.java | 2 +- src/com/android/tv/util/BooleanSystemProperty.java | 9 +- src/com/android/tv/util/RecurringRunner.java | 125 ++++ src/com/android/tv/util/SearchManagerHelper.java | 16 +- src/com/android/tv/util/SystemProperties.java | 19 + src/com/android/tv/util/TvInputManagerHelper.java | 127 ++-- src/com/android/tv/util/Utils.java | 54 +- 64 files changed, 3283 insertions(+), 1613 deletions(-) create mode 100644 src/com/android/tv/menu/IMenuView.java create mode 100644 src/com/android/tv/menu/Menu.java create mode 100644 src/com/android/tv/menu/MenuLayoutManager.java create mode 100644 src/com/android/tv/menu/MenuRowFactory.java create mode 100644 src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java create mode 100644 src/com/android/tv/util/RecurringRunner.java (limited to 'src/com/android') diff --git a/src/com/android/tv/ChannelTuner.java b/src/com/android/tv/ChannelTuner.java index 3b5b631d..f5114193 100644 --- a/src/com/android/tv/ChannelTuner.java +++ b/src/com/android/tv/ChannelTuner.java @@ -21,6 +21,7 @@ import android.database.Cursor; import android.media.tv.TvContract; import android.net.Uri; import android.os.Handler; +import android.support.annotation.WorkerThread; import android.util.Log; import com.android.tv.data.Channel; @@ -392,6 +393,7 @@ public class ChannelTuner { * @param channelId The ID of the channel to be loaded. * @return a channel if it has been loaded. {@code null} if the channel is not found. */ + @WorkerThread public Channel loadChannel(long channelId) { if (channelId < 0) { return null; diff --git a/src/com/android/tv/LauncherActivity.java b/src/com/android/tv/LauncherActivity.java index b2ecf726..e03952da 100644 --- a/src/com/android/tv/LauncherActivity.java +++ b/src/com/android/tv/LauncherActivity.java @@ -87,9 +87,9 @@ public class LauncherActivity extends Activity { // We should launch the new activity in onCreate rather than in onStart. // That's because it is not guaranteed that onStart is called only once. Intent intent = getIntent().getParcelableExtra(EXTRA_INTENT); - boolean requstResult = getIntent().getBooleanExtra(EXTRA_REQUEST_RESULT, false); + boolean requestResult = getIntent().getBooleanExtra(EXTRA_REQUEST_RESULT, false); try { - if (requstResult) { + if (requestResult) { startActivityForResult(intent, REQUEST_START_ACTIVITY); } else { startActivity(intent); diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java index f06225c0..db4cbffd 100644 --- a/src/com/android/tv/MainActivity.java +++ b/src/com/android/tv/MainActivity.java @@ -17,7 +17,6 @@ package com.android.tv; import android.app.Activity; -import android.app.ActivityManager; import android.app.FragmentTransaction; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; @@ -45,6 +44,7 @@ import android.os.Handler; import android.os.Message; import android.provider.Settings; import android.support.annotation.IntDef; +import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; import android.view.Display; @@ -62,7 +62,7 @@ import android.widget.Toast; import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.Tracker; -import com.android.tv.customization.TvCustomizationManager; +import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.OnCurrentProgramUpdatedListener; @@ -71,7 +71,7 @@ import com.android.tv.data.ProgramDataManager; import com.android.tv.data.StreamInfo; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dialog.SafeDismissDialogFragment; -import com.android.tv.menu.MenuView; +import com.android.tv.menu.Menu; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.parental.ParentalControlSettings; import com.android.tv.receiver.AudioCapabilitiesReceiver; @@ -86,7 +86,6 @@ import com.android.tv.ui.SelectInputView; import com.android.tv.ui.TunableTvView; import com.android.tv.ui.TunableTvView.OnTuneListener; import com.android.tv.ui.TvOverlayManager; -import com.android.tv.ui.TvTransitionManager; import com.android.tv.ui.TvViewUiManager; import com.android.tv.ui.sidepanel.ChannelSourcesFragment; import com.android.tv.ui.sidepanel.ClosedCaptionFragment; @@ -143,6 +142,7 @@ 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; + // Tracker screen names. public static final String SCREEN_NAME = "Main"; private static final String SCREEN_BEHIND_NAME = "Behind"; @@ -171,7 +171,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private static final String MEDIA_SESSION_TAG = "com.android.tv.mediasession"; // Change channels with key long press. - private static final boolean USE_ACCELERATION_IN_CHANNEL_CHANGE = true; private static final int CHANNEL_CHANGE_NORMAL_SPEED_DURATION_MS = 3000; private static final int CHANNEL_CHANGE_DELAY_MS_IN_MAX_SPEED = 50; private static final int CHANNEL_CHANGE_DELAY_MS_IN_NORMAL_SPEED = 200; @@ -216,7 +215,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private TunableTvView mPipView; private OverlayRootView mOverlayRootView; private Bundle mTuneParams; - private TvCustomizationManager mTvCustomizationManager; private boolean mChannelBannerHiddenBySideFragment; // TODO: Move the scene views into TvTransitionManager or TvOverlayManager. private ChannelBannerView mChannelBannerView; @@ -250,9 +248,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private boolean mIsFilmModeSet; private float mDefaultRefreshRate; - // TODO: Merge the TvTransitionManager into TvOverlayManager because the scene is a kind of - // overlay. - private TvTransitionManager mTransitionManager; private TvOverlayManager mOverlayManager; // mIsCurrentChannelUnblockedByUser and mWasChannelUnblockedBeforeShrunkenByUser are used for @@ -280,34 +275,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC // A caller which started this activity. (e.g. TvSearch) private String mSource; - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_CHANNEL_DOWN_PRESSED: - long startTime = (Long) msg.obj; - moveToAdjacentChannel(false, true); - mHandler.sendMessageDelayed(Message.obtain(msg), getDelay(startTime)); - break; - case MSG_CHANNEL_UP_PRESSED: - startTime = (Long) msg.obj; - moveToAdjacentChannel(true, true); - mHandler.sendMessageDelayed(Message.obtain(msg), getDelay(startTime)); - break; - case MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE: - updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO); - break; - } - } - - private long getDelay(long startTime) { - if (USE_ACCELERATION_IN_CHANNEL_CHANGE && System.currentTimeMillis() - startTime > - CHANNEL_CHANGE_NORMAL_SPEED_DURATION_MS) { - return CHANNEL_CHANGE_DELAY_MS_IN_MAX_SPEED; - } - return CHANNEL_CHANGE_DELAY_MS_IN_NORMAL_SPEED; - } - }; + private Handler mHandler = new MainActivityHandler(this); private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override @@ -358,6 +326,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC new ChannelTuner.Listener() { @Override public void onLoadFinished() { + markNewChannelsBrowsable(); if (mActivityResumed) { resumeTvIfNeeded(); resumePipIfNeeded(); @@ -366,7 +335,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mHandler.post(new Runnable() { @Override public void run() { - mOverlayManager.getMenuView().setChannelTuner(mChannelTuner); + mOverlayManager.getMenu().setChannelTuner(mChannelTuner); } }); } @@ -405,15 +374,17 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC @Override protected void onCreate(Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG,"onCreate"); + if (DEBUG) Log.d(TAG,"onCreate()"); + if(BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) { + Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show(); + } super.onCreate(savedInstanceState); TvApplication tvApplication = (TvApplication) getApplication(); tvApplication.setMainActivity(this); mTracker = tvApplication.getTracker(); - mTvInputManagerHelper = new TvInputManagerHelper(this); - mTvInputManagerHelper.start(); - mChannelDataManager = new ChannelDataManager(this, mTvInputManagerHelper); + mTvInputManagerHelper = tvApplication.getTvInputManagerHelper(); + mChannelDataManager = new ChannelDataManager(this, mTvInputManagerHelper, mTracker); mProgramDataManager = new ProgramDataManager(this); mProgramDataManager.addOnCurrentProgramUpdatedListener(Channel.INVALID_ID, mOnCurrentProgramUpdatedListener); @@ -463,7 +434,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return false; } }); - mTimeShiftManager = new TimeShiftManager(this, mTvView, mProgramDataManager, + mTimeShiftManager = new TimeShiftManager(this, mTvView, mProgramDataManager, mTracker, new OnCurrentProgramUpdatedListener() { @Override public void onCurrentProgramUpdated(long channelId, Program program) { @@ -502,14 +473,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC .inflate(R.layout.input_banner, sceneContainer, false); SelectInputView selectInputView = (SelectInputView) getLayoutInflater() .inflate(R.layout.select_input, sceneContainer, false); - mTransitionManager = new TvTransitionManager(this, sceneContainer, mChannelBannerView, - inputBannerView, mKeypadChannelSwitchView, selectInputView); mSearchFragment = new ProgramGuideSearchFragment(); - mOverlayManager = new TvOverlayManager(this, mChannelTuner, mTransitionManager, - mKeypadChannelSwitchView, selectInputView, mSearchFragment); - - mTvCustomizationManager = new TvCustomizationManager(this); - mTvCustomizationManager.initialize(); + mOverlayManager = new TvOverlayManager(this, mChannelTuner, + mKeypadChannelSwitchView, mChannelBannerView, inputBannerView, + selectInputView, sceneContainer, mSearchFragment); mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; @@ -562,10 +529,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (isUnderShrunkenTvView()) { return TunableTvView.BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW; } - if (mOverlayManager.getSideFragmentManager().isActive() - || mOverlayManager.getMenuView().isActive() - || mTransitionManager.isKeypadChannelSwitchActive() - || mTransitionManager.isSelectInputActive()) { + if (mOverlayManager.needHideTextOnMainView()) { return TunableTvView.BLOCK_SCREEN_TYPE_NO_UI; } SafeDismissDialogFragment currentDialog = mOverlayManager.getCurrentDialog(); @@ -596,6 +560,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC @Override protected void onStart() { + if (DEBUG) Log.d(TAG,"onStart()"); super.onStart(); mActivityStarted = true; mTracker.sendMainStart(); @@ -611,14 +576,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC Intent notificationIntent = new Intent(this, NotificationService.class); notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION); startService(notificationIntent); - - mOverlayManager.onStart(); } @Override protected void onResume() { - super.onResume(); if (DEBUG) Log.d(TAG, "onResume()"); + super.onResume(); mTracker.sendScreenView(SCREEN_NAME); SystemProperties.updateSystemProperties(); @@ -635,6 +598,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC // visible behind. requestVisibleBehind(true); } + if (mChannelTuner.areAllChannelsLoaded()) { + markNewChannelsBrowsable(); + } resumeTvIfNeeded(); resumePipIfNeeded(); mOverlayManager.showMenuWithTimeShiftPauseIfNeeded(); @@ -666,7 +632,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC // the screen. @Override public void run() { - showSelectInputView(); + mOverlayManager.showSelectInputView(); } }); } @@ -761,6 +727,33 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } + private void markNewChannelsBrowsable() { + SetupUtils setupUtils = SetupUtils.getInstance(MainActivity.this); + Set newInputsWithChannels = new HashSet<>(); + for (TvInputInfo input : mTvInputManagerHelper.getTvInputInfos(true, true)) { + String inputId = input.getId(); + if (!setupUtils.hasSetupLaunched(inputId) + && mChannelDataManager.getChannelCountForInput(inputId) > 0) { + setupUtils.onSetupLaunched(inputId); + setupUtils.markAsKnownInput(inputId); + newInputsWithChannels.add(inputId); + if (DEBUG) { + Log.d(TAG, "New input " + inputId + " has " + + mChannelDataManager.getChannelCountForInput(inputId) + + " channels"); + } + } + } + if (!newInputsWithChannels.isEmpty()) { + for (Channel channel : mChannelDataManager.getChannelList()) { + if (newInputsWithChannels.contains(channel.getInputId())) { + mChannelDataManager.updateBrowsable(channel.getId(), true); + } + } + mChannelDataManager.applyUpdatedValuesToDb(); + } + } + private Channel loadInitialChannel(Uri channelUri) { if (TvContract.isChannelUriForPassthroughInput(channelUri)) { throw new IllegalArgumentException("channelUri should be null or tuner input channel"); @@ -853,7 +846,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mMediaSession = null; } mActivityStarted = false; - mOverlayManager.onStop(); stopAll(false); unregisterReceiver(mBroadcastReceiver); mTracker.sendMainStop(mMainDurationTimer.reset()); @@ -875,11 +867,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC * * @param calledByDialog If true, startSetupActivity is invoked from the setup dialog. */ - public boolean startSetupActivity(TvInputInfo input, boolean calledByDialog) { + public void startSetupActivity(TvInputInfo input, boolean calledByDialog) { Intent intent = input.createSetupIntent(); if (intent == null) { Toast.makeText(this, R.string.msg_no_setup_activity, Toast.LENGTH_SHORT).show(); - return false; + return; } try { // Now we know that the user intends to set up this input. Grant permission for writing @@ -894,7 +886,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mInputIdUnderSetup = null; Toast.makeText(this, getString(R.string.msg_unable_to_start_setup_activity, input.loadLabel(this)), Toast.LENGTH_SHORT).show(); - return false; + return; } if (calledByDialog) { mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION @@ -904,7 +896,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY); } stopTv("startSetupActivity()", false); - return true; } public boolean hasCaptioningSettingsActivity() { @@ -1465,6 +1456,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC + " with the URI " + channel); return; } + if (isChannelChangeKeyDownReceived()) { + // Ignore this message if the user is changing the channel. + return; + } mPipChannel = currentChannel; mPipView.setCurrentChannel(mPipChannel); } @@ -1534,19 +1529,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return; } if (mChannelDataManager.getChannelCount() > 0) { - Set inputIds = new HashSet<>(); - // Enable all channels. - for (Channel channel : mChannelDataManager.getChannelList()) { - mChannelDataManager.updateBrowsable(channel.getId(), true); - inputIds.add(channel.getInputId()); - } - mChannelDataManager.applyUpdatedValuesToDb(); - // Move to a first channel - mChannelTuner.moveToAdjacentBrowsableChannel(true); - for (String inputId : inputIds) { - setupUtils.onSetupLaunched(inputId); - setupUtils.markAsKnownInput(inputId); - } mOverlayManager.showIntroDialog(); } else { mOverlayManager.showSetupDialog(); @@ -1664,7 +1646,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC applyMultiAudio(); applyClosedCaption(); // TODO: Send command to TIS with checking the settings in TV and CaptionManager. - mOverlayManager.getMenuView().updateOptionsRow(); + mOverlayManager.getMenu().onStreamInfoChanged(); if (mTvView.isVideoAvailable()) { mTvViewUiManager.fadeInTvView(); } @@ -1684,6 +1666,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC + channel); return; } + if (isChannelChangeKeyDownReceived()) { + // Ignore this message if the user is changing the channel. + return; + } mChannelTuner.setCurrentChannel(currentChannel); mTvView.setCurrentChannel(currentChannel); updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE); @@ -1747,7 +1733,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } mRecentChannels.addFirst(channelId); - mOverlayManager.getMenuView().onRecentChannelUpdated(); + mOverlayManager.getMenu().onRecentChannelsChanged(); } /** @@ -1846,7 +1832,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mChannelBannerHiddenBySideFragment = true; } else { mChannelBannerHiddenBySideFragment = false; - mTransitionManager.goToChannelBannerScene(); + mOverlayManager.showBanner(); } } updateAvailabilityToast(); @@ -1980,20 +1966,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC */ public void showKeypadChannelSwitchView(int keyCode) { if (mChannelTuner.areAllChannelsLoaded()) { - // Show KeypadChannelSwitchView only if all the channels are loaded. - mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG); - mTransitionManager.goToKeypadChannelSwitchScene(); + mOverlayManager.showKeypadChannelSwitch(); mKeypadChannelSwitchView.onNumberKeyUp(keyCode - KeyEvent.KEYCODE_0); } } - public void showSelectInputView() { - mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE); - mTransitionManager.goToSelectInputScene(); - } - 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. @@ -2022,14 +1999,14 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC Channel.INVALID_ID, mOnCurrentProgramUpdatedListener); mProgramDataManager.stop(); mChannelDataManager.stop(); - mTvInputManagerHelper.stop(); mPipInputManager.stop(); - mOverlayManager.getMenuView().setChannelTuner(null); + mOverlayManager.release(); mChannelTuner.stop(); mKeypadChannelSwitchView.setChannels(null); mMemoryManageables.clear(); ((TvApplication) getApplication()).setMainActivity(null); mAudioCapabilitiesReceiver.unregister(); + mHandler.removeCallbacksAndMessages(null); super.onDestroy(); } @@ -2202,7 +2179,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW); } if (keyCode != KeyEvent.KEYCODE_E) { - mOverlayManager.showMenu(MenuView.REASON_NONE); + mOverlayManager.showMenu(Menu.REASON_NONE); } return true; case KeyEvent.KEYCODE_CHANNEL_UP: @@ -2310,16 +2287,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mOverlayManager.onUserInteraction(); } - /** - * Returns TvCustomizationManager. - */ - public TvCustomizationManager getTvCustomizationManager() { - return mTvCustomizationManager; - } - public void togglePipView() { enablePipView(!mPipEnabled, true); - mOverlayManager.getMenuView().update(); + mOverlayManager.getMenu().update(); } public boolean isPipEnabled() { @@ -2377,9 +2347,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } + private boolean isChannelChangeKeyDownReceived() { + return mHandler.hasMessages(MSG_CHANNEL_UP_PRESSED) + || mHandler.hasMessages(MSG_CHANNEL_DOWN_PRESSED); + } + private void finishChannelChangeIfNeeded() { - if (!mHandler.hasMessages(MSG_CHANNEL_UP_PRESSED) && !mHandler.hasMessages( - MSG_CHANNEL_DOWN_PRESSED)) { + if (!isChannelChangeKeyDownReceived()) { return; } mHandler.removeMessages(MSG_CHANNEL_UP_PRESSED); @@ -2655,7 +2629,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } private void updateAvailabilityToast(StreamInfo info) { - if (mTransitionManager.isSceneActive() || info.isVideoAvailable()) { + if (info.isVideoAvailable()) { return; } @@ -2694,14 +2668,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return mCaptionSettings; } - public void goToEmptyScene(boolean withAnimation) { - mTransitionManager.goToEmptyScene(withAnimation); - } - // Initialize TV app for test. The setup process should be finished before the Live TV app is // started. We only enable all the channels here. private void initForTest() { - if (!ActivityManager.isRunningInTestHarness()) { + if (!Utils.isRunningInTest()) { return; } @@ -2727,7 +2697,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private void initAnimations() { mTvViewUiManager.initAnimatorIfNeeded(); - mTransitionManager.initIfNeeded(); + mOverlayManager.initAnimatorIfNeeded(); } private void initSideFragments() { @@ -2754,4 +2724,37 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC */ void performTrimMemory(int level); } + + private static class MainActivityHandler extends WeakHandler { + MainActivityHandler(MainActivity mainActivity) { + super(mainActivity); + } + + @Override + protected void handleMessage(Message msg, @NonNull MainActivity mainActivity) { + switch (msg.what) { + case MSG_CHANNEL_DOWN_PRESSED: + long startTime = (Long) msg.obj; + mainActivity.moveToAdjacentChannel(false, true); + sendMessageDelayed(Message.obtain(msg), getDelay(startTime)); + break; + case MSG_CHANNEL_UP_PRESSED: + startTime = (Long) msg.obj; + mainActivity.moveToAdjacentChannel(true, true); + sendMessageDelayed(Message.obtain(msg), getDelay(startTime)); + break; + case MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE: + mainActivity.updateChannelBannerAndShowIfNeeded( + UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO); + break; + } + } + + private long getDelay(long startTime) { + if (System.currentTimeMillis() - startTime > CHANNEL_CHANGE_NORMAL_SPEED_DURATION_MS) { + return CHANNEL_CHANGE_DELAY_MS_IN_MAX_SPEED; + } + return CHANNEL_CHANGE_DELAY_MS_IN_NORMAL_SPEED; + } + } } diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java index e606b417..5a7b51c1 100644 --- a/src/com/android/tv/TimeShiftManager.java +++ b/src/com/android/tv/TimeShiftManager.java @@ -16,7 +16,6 @@ package com.android.tv; -import android.annotation.SuppressLint; import android.content.ContentResolver; import android.content.Context; import android.os.Handler; @@ -28,6 +27,8 @@ import android.support.annotation.VisibleForTesting; import android.util.Log; import android.util.Range; +import com.android.tv.analytics.Tracker; +import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; import com.android.tv.data.OnCurrentProgramUpdatedListener; import com.android.tv.data.Program; @@ -145,6 +146,7 @@ public class TimeShiftManager { private final PlayController mPlayController; private final ProgramManager mProgramManager; + private final Tracker mTracker; @VisibleForTesting final CurrentPositionMediator mCurrentPositionMediator = new CurrentPositionMediator(); @@ -153,6 +155,7 @@ public class TimeShiftManager { private int mEnabledActionIds = TIME_SHIFT_ACTION_ID_PLAY | TIME_SHIFT_ACTION_ID_PAUSE | TIME_SHIFT_ACTION_ID_REWIND | TIME_SHIFT_ACTION_ID_FAST_FORWARD | TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS | TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT; + @TimeShiftActionId private int mLastActionId = 0; // TODO: Remove these variables once API level 23 is available. @@ -162,27 +165,15 @@ public class TimeShiftManager { // This variable is used to block notification while changing the availability status. private boolean mNotificationEnabled; - @SuppressLint("HandlerLeak") - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_GET_CURRENT_POSITION: - mPlayController.handleGetCurrentPosition(); - break; - case MSG_PREFETCH_PROGRAM: - mProgramManager.prefetchPrograms(); - break; - } - } - }; + private final Handler mHandler = new TimeShiftHandler(this); public TimeShiftManager(Context context, TunableTvView tvView, - ProgramDataManager programDataManager, + ProgramDataManager programDataManager, Tracker tracker, OnCurrentProgramUpdatedListener onCurrentProgramUpdatedListener) { mContext = context; mPlayController = new PlayController(tvView); mProgramManager = new ProgramManager(programDataManager); + mTracker = tracker; mOnCurrentProgramUpdatedListener = onCurrentProgramUpdatedListener; tvView.setOnScreenBlockedListener(new TunableTvView.OnScreenBlockingChangedListener() { @Override @@ -235,6 +226,7 @@ public class TimeShiftManager { if (!isActionEnabled(TIME_SHIFT_ACTION_ID_PLAY)) { return; } + mTracker.sendTimeShiftAction(TIME_SHIFT_ACTION_ID_PLAY); mLastActionId = TIME_SHIFT_ACTION_ID_PLAY; mPlayController.play(); updateActions(); @@ -250,6 +242,7 @@ public class TimeShiftManager { return; } mLastActionId = TIME_SHIFT_ACTION_ID_PAUSE; + mTracker.sendTimeShiftAction(mLastActionId); mPlayController.pause(); updateActions(); } @@ -275,6 +268,7 @@ public class TimeShiftManager { return; } mLastActionId = TIME_SHIFT_ACTION_ID_REWIND; + mTracker.sendTimeShiftAction(mLastActionId); mPlayController.rewind(); updateActions(); } @@ -291,6 +285,7 @@ public class TimeShiftManager { return; } mLastActionId = TIME_SHIFT_ACTION_ID_FAST_FORWARD; + mTracker.sendTimeShiftAction(mLastActionId); mPlayController.fastForward(); updateActions(); } @@ -316,6 +311,7 @@ public class TimeShiftManager { long seekPosition = Math.max(program.getStartTimeUtcMillis(), mPlayController.mRecordStartTimeMs); mLastActionId = TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS; + mTracker.sendTimeShiftAction(mLastActionId); mPlayController.seekTo(seekPosition); mCurrentPositionMediator.onSeekRequested(seekPosition); updateActions(); @@ -340,6 +336,7 @@ public class TimeShiftManager { Program nextProgram = mProgramManager.getProgramAt(currentProgram.getEndTimeUtcMillis()); long currentTimeMs = System.currentTimeMillis(); mLastActionId = TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT; + mTracker.sendTimeShiftAction(mLastActionId); if (nextProgram == null || nextProgram.getStartTimeUtcMillis() > currentTimeMs) { mPlayController.seekTo(currentTimeMs); if (mPlayController.isForwarding()) { @@ -721,8 +718,10 @@ public class TimeShiftManager { void togglePlayPause() { if (mPlayStatus == PLAY_STATUS_PAUSED) { play(); + mTracker.sendTimeShiftAction(TIME_SHIFT_ACTION_ID_PLAY); } else { pause(); + mTracker.sendTimeShiftAction(TIME_SHIFT_ACTION_ID_PAUSE); } } @@ -1292,4 +1291,22 @@ public class TimeShiftManager { */ void onActionEnabledChanged(@TimeShiftActionId int actionId, boolean enabled); } + + private static class TimeShiftHandler extends WeakHandler { + public TimeShiftHandler(TimeShiftManager ref) { + super(ref); + } + + @Override + public void handleMessage(Message msg, @NonNull TimeShiftManager timeShiftManager) { + switch (msg.what) { + case MSG_GET_CURRENT_POSITION: + timeShiftManager.mPlayController.handleGetCurrentPosition(); + break; + case MSG_PREFETCH_PROGRAM: + timeShiftManager.mProgramManager.prefetchPrograms(); + break; + } + } + } } diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java index 4f73f1ec..d3a8facb 100644 --- a/src/com/android/tv/TvApplication.java +++ b/src/com/android/tv/TvApplication.java @@ -24,15 +24,21 @@ import android.content.pm.PackageManager; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.os.Bundle; +import android.os.StrictMode; import android.util.Log; import android.view.KeyEvent; import com.android.tv.analytics.Analytics; import com.android.tv.analytics.StubAnalytics; +import com.android.tv.analytics.StubAnalytics; import com.android.tv.analytics.Tracker; +import com.android.tv.util.RecurringRunner; +import com.android.tv.util.SystemProperties; +import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; import java.util.List; +import java.util.concurrent.TimeUnit; public class TvApplication extends Application { private static final String TAG = "TvApplication"; @@ -41,11 +47,31 @@ public class TvApplication extends Application { private MainActivity mActivity; private Tracker mTracker; + private TvInputManagerHelper mTvInputManagerHelper; + private RecurringRunner mSendConfigInfoRecurringRunner; @Override public void onCreate() { super.onCreate(); - Analytics analytics = StubAnalytics.getInstance(this); + // Only set StrictMode for ENG builds because the build server only produces userdebug + // builds. + if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) { + StrictMode.setThreadPolicy( + new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()); + StrictMode.VmPolicy.Builder vmPolicyBuilder = new StrictMode.VmPolicy.Builder() + .detectAll().penaltyLog(); + if (BuildConfig.ENG && SystemProperties.ALLOW_DEATH_PENALTY.getValue() && + !Utils.isRunningInTest()) { + // TODO turn on death penalty for tests when they stop leaking MainActivity + } + StrictMode.setVmPolicy(vmPolicyBuilder.build()); + } + Analytics analytics; + if (BuildConfig.ENG && !SystemProperties.ALLOW_ANALYTICS_IN_ENG.getValue()) { + analytics = StubAnalytics.getInstance(this); + } else { + analytics = StubAnalytics.getInstance(this); + } mTracker = analytics.getDefaultTracker(); try { PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); @@ -54,13 +80,22 @@ public class TvApplication extends Application { Log.w(TAG, "Unable to get version name.", e); versionName = ""; } - if (DEBUG) Log.d(TAG, "Starting Live TV " + versionName); + mTvInputManagerHelper = new TvInputManagerHelper(this); + mTvInputManagerHelper.start(); + mSendConfigInfoRecurringRunner = new RecurringRunner(this, TimeUnit.DAYS.toMillis(1), + new SendConfigInfoRunnable()); + mSendConfigInfoRecurringRunner.start(); + if (DEBUG) Log.i(TAG, "Started Live TV " + versionName); } public Tracker getTracker() { return mTracker; } + public TvInputManagerHelper getTvInputManagerHelper() { + return mTvInputManagerHelper; + } + /** * MainActivity is set in {@link MainActivity#onCreate} and cleared in * {@link MainActivity#onDestroy}. @@ -132,4 +167,36 @@ public class TvApplication extends Application { public static String getVersionName() { return versionName; } + + /** + * Data useful for tracking that doesn't change often. + */ + public static class ConfigurationInfo { + public final int systemInputCount; + public final int nonSystemInputCount; + + public ConfigurationInfo(int systemInputCount, int nonSystemInputCount) { + this.systemInputCount = systemInputCount; + this.nonSystemInputCount = nonSystemInputCount; + } + } + + private class SendConfigInfoRunnable implements Runnable { + @Override + public void run() { + List infoList = mTvInputManagerHelper.getTvInputInfos(false, false); + int systemInputCount = 0; + int nonSystemInputCount = 0; + for (TvInputInfo info : infoList) { + if (mTvInputManagerHelper.isSystemInput(info)) { + systemInputCount++; + } else { + nonSystemInputCount++; + } + } + ConfigurationInfo configurationInfo = new ConfigurationInfo(systemInputCount, + nonSystemInputCount); + mTracker.sendConfigurationInfo(configurationInfo); + } + } } diff --git a/src/com/android/tv/analytics/HasTrackerLabel.java b/src/com/android/tv/analytics/HasTrackerLabel.java index 63466ad5..566e5f1a 100644 --- a/src/com/android/tv/analytics/HasTrackerLabel.java +++ b/src/com/android/tv/analytics/HasTrackerLabel.java @@ -26,5 +26,5 @@ public interface HasTrackerLabel { /** * Returns the label. */ - public String getTrackerLabel(); + String getTrackerLabel(); } diff --git a/src/com/android/tv/analytics/StubTracker.java b/src/com/android/tv/analytics/StubTracker.java index d271ae83..f7efcb92 100644 --- a/src/com/android/tv/analytics/StubTracker.java +++ b/src/com/android/tv/analytics/StubTracker.java @@ -16,12 +16,23 @@ package com.android.tv.analytics; +import android.support.annotation.VisibleForTesting; + +import com.android.tv.TimeShiftManager; +import com.android.tv.TvApplication; import com.android.tv.data.Channel; /** * A implementation of Tracker that does nothing. */ +@VisibleForTesting public class StubTracker implements Tracker { + @Override + public void sendChannelCount(int browsableChannelCount, int totalChannelCount) { } + + @Override + public void sendConfigurationInfo(TvApplication.ConfigurationInfo info) { } + @Override public void sendMainStart() { } @@ -32,7 +43,7 @@ public class StubTracker implements Tracker { public void sendScreenView(String screenName) { } @Override - public void sendChannelViewStart(Channel channel) { } + public void sendChannelViewStart(Channel channel, boolean tunedByRecommendation) { } @Override public void sendChannelTuneTime(Channel channel, long durationMs) { } @@ -102,4 +113,7 @@ public class StubTracker implements Tracker { @Override public void sendHideSidePanel(HasTrackerLabel trackerLabel, long durationMs) { } + + @Override + public void sendTimeShiftAction(@TimeShiftManager.TimeShiftActionId int actionId) { } } diff --git a/src/com/android/tv/analytics/Tracker.java b/src/com/android/tv/analytics/Tracker.java index e2160e86..05638871 100644 --- a/src/com/android/tv/analytics/Tracker.java +++ b/src/com/android/tv/analytics/Tracker.java @@ -16,12 +16,36 @@ package com.android.tv.analytics; +import com.android.tv.TimeShiftManager; +import com.android.tv.TvApplication; import com.android.tv.data.Channel; /** * Interface for sending user activity for analysis. */ public interface Tracker { + + /** + * Send the number of channels that doesn't change often. + * + *

Because the number of channels does not change often, this method should not be called + * more than once a day. + * + * @param browsableChannelCount the number of browsable channels. + * @param totalChannelCount the number of all channels. + */ + void sendChannelCount(int browsableChannelCount, int totalChannelCount); + + /** + * Send data that doesn't change often. + * + *

Because configuration info does not change often, this method should not be called more + * than once a day. + * + * @param info the configuration info. + */ + void sendConfigurationInfo(TvApplication.ConfigurationInfo info); + /** * Sends tracking information for starting the MainActivity. */ @@ -43,8 +67,9 @@ public interface Tracker { * Sends tracking information for starting to view a channel. * * @param channel the current channel + * @param tunedByRecommendation True, if the channel was tuned by the recommendation. */ - void sendChannelViewStart(Channel channel); + void sendChannelViewStart(Channel channel, boolean tunedByRecommendation); /** * Sends tracking information for tuning to a channel. @@ -103,17 +128,17 @@ public interface Tracker { void sendMenuClicked(int labelResId); /** - * Sends tracking information for showing the Enhanced Program Guide (EPG). + * Sends tracking information for showing the Electronic Program Guide (EPG). */ void sendShowEpg(); /** - * Sends tracking information for clicking an Enhanced Program Guide (EPG) item. + * Sends tracking information for clicking an Electronic Program Guide (EPG) item. */ void sendEpgItemClicked(); /** - * Sends tracking for hiding the Enhanced Program Guide (EPG). + * Sends tracking for hiding the Electronic Program Guide (EPG). * * @param durationMs The duration the EPG was shown in milliseconds. */ @@ -154,7 +179,7 @@ public interface Tracker { void sendChannelNumberItemChosenByTimeout(); /** - * Sends HDMI AC3 passthrough capablities. + * Sends HDMI AC3 passthrough capabilities. * * @param isSupported {@code true} if the feature is supported; otherwise {@code false}. */ @@ -195,4 +220,11 @@ public interface Tracker { * @param durationMs The duration the side panel was shown in milliseconds. */ void sendHideSidePanel(HasTrackerLabel trackerLabel, long durationMs); + + /** + * Sends time shift action (pause, ff, etc). + * + * @param actionId The label of the side panel + */ + void sendTimeShiftAction(@TimeShiftManager.TimeShiftActionId int actionId); } diff --git a/src/com/android/tv/customization/CustomAction.java b/src/com/android/tv/customization/CustomAction.java index 3263d170..b8f4695b 100644 --- a/src/com/android/tv/customization/CustomAction.java +++ b/src/com/android/tv/customization/CustomAction.java @@ -49,15 +49,6 @@ public class CustomAction implements Comparable { return mPositionPriority < POSITION_THRESHOLD; } - /** - * Returns position priority defined in partner customization package. - * If there’s multiple custom options are at the front or back, - * options in each group will be sorted by their priority in ascending order. - */ - public int getPositionPriority() { - return mPositionPriority; - } - @Override public int compareTo(@NonNull CustomAction another) { return mPositionPriority - another.mPositionPriority; diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java index 659eab02..49244c14 100644 --- a/src/com/android/tv/data/Channel.java +++ b/src/com/android/tv/data/Channel.java @@ -24,6 +24,7 @@ import android.graphics.Bitmap; import android.media.tv.TvContract; import android.media.tv.TvInputInfo; import android.net.Uri; +import android.os.Build; import android.support.annotation.UiThread; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; @@ -73,7 +74,7 @@ public final class Channel { private static final String INVALID_PACKAGE_NAME = "packageName"; private static final String[] PROJECTION_BASE = { - // Columns should match what is read in Channel.fromCursor() + // Columns must match what is read in Channel.fromCursor() TvContract.Channels._ID, TvContract.Channels.COLUMN_PACKAGE_NAME, TvContract.Channels.COLUMN_INPUT_ID, @@ -99,7 +100,7 @@ public final class Channel { public static final String[] PROJECTION = createProjection(); private static String[] createProjection() { - if (TvCommonConstants.IS_MNC_OR_HIGHER) { + if (Build.VERSION.SDK_INT >= 23) { ArrayList temp = new ArrayList<>( PROJECTION_BASE.length + PROJECTION_ADDED_IN_MNC.length); temp.addAll(Arrays.asList(PROJECTION_BASE)); @@ -110,6 +111,36 @@ public final class Channel { } } + /** + * Creates {@code Channel} object from cursor. + * + *

The query that created the cursor MUST use {@link #PROJECTION} + * + */ + public static Channel fromCursor(Cursor cursor) { + // Columns read must match the order of {@link #PROJECTION} + Channel channel = new Channel(); + int index = 0; + channel.mId = cursor.getLong(index++); + channel.mPackageName = Utils.intern(cursor.getString(index++)); + channel.mInputId = Utils.intern(cursor.getString(index++)); + channel.mType = Utils.intern(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++)); + channel.mBrowsable = cursor.getInt(index++) == 1; + channel.mLocked = cursor.getInt(index++) == 1; + if (Build.VERSION.SDK_INT >= 23) { + channel.mAppLinkText = cursor.getString(index++); + channel.mAppLinkColor = cursor.getInt(index++); + channel.mAppLinkIconUri = cursor.getString(index++); + channel.mAppLinkPosterArtUri = cursor.getString(index++); + channel.mAppLinkIntentUri = cursor.getString(index++); + } + return channel; + } + /** ID of this channel. Matches to BaseColumns._ID. */ private long mId; @@ -135,105 +166,6 @@ public final class Channel { void onLoadImageFinished(Channel channel, int type, Bitmap logo); } - /** - * Creates {@code Channel} object from cursor. - * Suppress using this outside of ChannelDataManager - * so Channels could be managed by ChannelDataManager. - */ - public static Channel fromCursor(Cursor cursor) { - // Columns read here should match Channel.PROJECTION - - Channel channel = new Channel(); - int index = cursor.getColumnIndex(TvContract.Channels._ID); - if (index >= 0) { - channel.mId = cursor.getLong(index); - } else { - channel.mId = INVALID_ID; - } - - index = cursor.getColumnIndex(TvContract.Channels.COLUMN_PACKAGE_NAME); - if (index >= 0) { - channel.mPackageName = Utils.intern(cursor.getString(index)); - } else { - channel.mPackageName = INVALID_PACKAGE_NAME; - } - - index = cursor.getColumnIndex(TvContract.Channels.COLUMN_INPUT_ID); - if (index >= 0) { - channel.mInputId = Utils.intern(cursor.getString(index)); - } else { - channel.mInputId = "inputId"; - } - - index = cursor.getColumnIndex(TvContract.Channels.COLUMN_TYPE); - if (index >= 0) { - channel.mType = Utils.intern(cursor.getString(index)); - } else { - channel.mType = "type"; - } - - index = cursor.getColumnIndex(TvContract.Channels.COLUMN_DISPLAY_NUMBER); - if (index >= 0) { - channel.mDisplayNumber = cursor.getString(index); - } else { - channel.mDisplayNumber = "0"; - } - - index = cursor.getColumnIndex(TvContract.Channels.COLUMN_DISPLAY_NAME); - if (index >= 0) { - channel.mDisplayName = cursor.getString(index); - } else { - channel.mDisplayName = "name"; - } - - index = cursor.getColumnIndex(TvContract.Channels.COLUMN_DESCRIPTION); - if (index >= 0) { - channel.mDescription = cursor.getString(index); - } else { - channel.mDescription = "description"; - } - - index = cursor.getColumnIndex(TvContract.Channels.COLUMN_VIDEO_FORMAT); - if (index >= 0) { - channel.mVideoFormat = Utils.intern(cursor.getString(index)); - } else { - channel.mVideoFormat = ""; - } - - index = cursor.getColumnIndex(TvContract.Channels.COLUMN_BROWSABLE); - channel.mBrowsable = index < 0 || cursor.getInt(index) == 1; - - index = cursor.getColumnIndex(TvContract.Channels.COLUMN_LOCKED); - channel.mLocked = index < 0 || cursor.getInt(index) == 1; - if (TvCommonConstants.IS_MNC_OR_HIGHER) { - index = cursor.getColumnIndex(TvContract.Channels.COLUMN_APP_LINK_TEXT); - if (index >= 0) { - channel.mAppLinkText = cursor.getString(index); - } - - index = cursor.getColumnIndex(TvContract.Channels.COLUMN_APP_LINK_COLOR); - if (index >= 0) { - channel.mAppLinkColor = cursor.getInt(index); - } - - index = cursor.getColumnIndex(TvContract.Channels.COLUMN_APP_LINK_ICON_URI); - if (index >= 0) { - channel.mAppLinkIconUri = cursor.getString(index); - } - - index = cursor.getColumnIndex(TvContract.Channels.COLUMN_APP_LINK_POSTER_ART_URI); - if (index >= 0) { - channel.mAppLinkPosterArtUri = cursor.getString(index); - } - - index = cursor.getColumnIndex(TvContract.Channels.COLUMN_APP_LINK_INTENT_URI); - if (index >= 0) { - channel.mAppLinkIntentUri = cursor.getString(index); - } - } - return channel; - } - private Channel() { // Do nothing. } @@ -270,6 +202,7 @@ public final class Channel { return mDisplayName; } + @VisibleForTesting public String getDescription() { return mDescription; } @@ -491,6 +424,7 @@ public final class Channel { return this; } + @VisibleForTesting public Builder setDescription(String description) { mChannel.mDescription = description; return this; @@ -621,7 +555,7 @@ public final class Channel { PackageManager pm = context.getPackageManager(); if (!TextUtils.isEmpty(mAppLinkText) && !TextUtils.isEmpty(mAppLinkIntentUri)) { try { - Intent intent = Intent.parseUri(mAppLinkIntentUri, 0); + Intent intent = Intent.parseUri(mAppLinkIntentUri, Intent.URI_INTENT_SCHEME); if (intent.resolveActivityInfo(pm, 0) != null) { mAppLinkIntent = intent; mAppLinkIntent.putExtra(TvCommonConstants.EXTRA_APP_LINK_CHANNEL_URI, @@ -673,35 +607,36 @@ public final class Channel { @Override public int compare(Channel lhs, Channel rhs) { - if (Objects.equals(lhs.getInputId(), rhs.getInputId())) { - // Compare the channel numbers if both channels belong to the same input. - int compare = ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber()); - if (mDetectDuplicatesEnabled && compare == 0) { - Log.w(TAG, "Duplicate channels detected! - \"" - + lhs.getDisplayNumber() + " " + lhs.getDisplayName() + "\" and \"" - + rhs.getDisplayNumber() + " " + rhs.getDisplayName() + "\""); - } - return compare; - } else { - // Put channels from OEM/SOC inputs first. - boolean lhsIsPartner = mInputManager.isPartnerInput(lhs.getInputId()); - boolean rhsIsPartner = mInputManager.isPartnerInput(rhs.getInputId()); - if (lhsIsPartner != rhsIsPartner) { - return lhsIsPartner ? -1 : 1; - } - - // Otherwise, compare the input labels. - String lhsLabel = getInputLabelForChannel(lhs); - String rhsLabel = getInputLabelForChannel(rhs); - if (lhsLabel == null && rhsLabel != null) { - return 1; - } else if (lhsLabel != null && rhsLabel == null) { - return -1; - } else if (lhsLabel == null /* && rhsLabel == null */) { - return 0; - } - return lhsLabel.compareTo(rhsLabel); + if (lhs == rhs) { + return 0; + } + // Put channels from OEM/SOC inputs first. + boolean lhsIsPartner = mInputManager.isPartnerInput(lhs.getInputId()); + boolean rhsIsPartner = mInputManager.isPartnerInput(rhs.getInputId()); + if (lhsIsPartner != rhsIsPartner) { + return lhsIsPartner ? -1 : 1; + } + // Compare the input labels. + String lhsLabel = getInputLabelForChannel(lhs); + String rhsLabel = getInputLabelForChannel(rhs); + int result = lhsLabel == null ? (rhsLabel == null ? 0 : 1) : rhsLabel == null ? -1 + : lhsLabel.compareTo(rhsLabel); + if (result != 0) { + return result; + } + // Compare the input IDs. The input IDs cannot be null. + result = lhs.getInputId().compareTo(rhs.getInputId()); + if (result != 0) { + return result; + } + // Compare the channel numbers if both channels belong to the same input. + result = ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber()); + if (mDetectDuplicatesEnabled && result == 0) { + Log.w(TAG, "Duplicate channels detected! - \"" + + lhs.getDisplayNumber() + " " + lhs.getDisplayName() + "\" and \"" + + rhs.getDisplayNumber() + " " + rhs.getDisplayName() + "\""); } + return result; } @VisibleForTesting diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java index d09d1686..2325952f 100644 --- a/src/com/android/tv/data/ChannelDataManager.java +++ b/src/com/android/tv/data/ChannelDataManager.java @@ -26,11 +26,15 @@ import android.media.tv.TvInputManager.TvInputCallback; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.util.Log; import android.util.MutableInt; +import com.android.tv.analytics.Tracker; +import com.android.tv.common.WeakHandler; import com.android.tv.util.AsyncDbTask; +import com.android.tv.util.RecurringRunner; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -41,6 +45,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; /** * The class to manage channel data. @@ -54,6 +59,7 @@ public class ChannelDataManager { private static final boolean DEBUG = false; private static final int MSG_UPDATE_CHANNELS = 1000; + private static final long SEND_CHANNEL_STATUS_INTERVAL_MS = TimeUnit.DAYS.toMillis(1); private final Context mContext; private final TvInputManagerHelper mInputManager; @@ -61,6 +67,9 @@ public class ChannelDataManager { private boolean mDbLoadFinished; private QueryAllChannelsTask mChannelsUpdateTask; private final List mPostRunnablesAfterChannelUpdate = new ArrayList<>(); + // TODO: move ChannelDataManager to TvApplication to consistently run mRecurringRunner. + private RecurringRunner mRecurringRunner; + private final Tracker mTracker; private final Set mListeners = new HashSet<>(); private final Map mChannelWrapperMap = new HashMap<>(); @@ -123,12 +132,13 @@ public class ChannelDataManager { } }; - public ChannelDataManager(Context context, TvInputManagerHelper inputManager) { - this(context, inputManager, context.getContentResolver(), Looper.myLooper()); + public ChannelDataManager(Context context, TvInputManagerHelper inputManager, + Tracker tracker) { + this(context, inputManager, tracker, context.getContentResolver(), Looper.myLooper()); } @VisibleForTesting - ChannelDataManager(Context context, TvInputManagerHelper inputManager, + ChannelDataManager(Context context, TvInputManagerHelper inputManager, Tracker tracker, ContentResolver contentResolver, Looper looper) { mContext = context; mInputManager = inputManager; @@ -136,14 +146,7 @@ public class ChannelDataManager { mChannelComparator = new Channel.DefaultComparator(context, inputManager); // Detect duplicate channels while sorting. mChannelComparator.setDetectDuplicatesEnabled(true); - mHandler = new Handler(looper) { - @Override - public void handleMessage(Message msg) { - if (msg.what == MSG_UPDATE_CHANNELS) { - handleUpdateChannels(); - } - } - }; + mHandler = new ChannelDataManagerHandler(looper, this); mChannelObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { @@ -152,6 +155,9 @@ public class ChannelDataManager { } } }; + mTracker = tracker; + mRecurringRunner = new RecurringRunner(mContext, SEND_CHANNEL_STATUS_INTERVAL_MS, + new SendChannelStatusRunnable()); } @VisibleForTesting @@ -185,6 +191,7 @@ public class ChannelDataManager { } mStarted = false; mDbLoadFinished = false; + mRecurringRunner.stop(); ChannelLogoFetcher.stopFetchingChannelLogos(); mInputManager.removeCallback(mTvInputCallback); @@ -602,6 +609,7 @@ public class ChannelDataManager { if (!mDbLoadFinished) { mDbLoadFinished = true; + mRecurringRunner.start(); for (Listener l : mListeners) { l.onLoadFinished(); } @@ -641,4 +649,30 @@ public class ChannelDataManager { } }); } + + private static class ChannelDataManagerHandler extends WeakHandler { + public ChannelDataManagerHandler(Looper looper, ChannelDataManager channelDataManager) { + super(looper, channelDataManager); + } + + @Override + public void handleMessage(Message msg, @NonNull ChannelDataManager channelDataManager) { + if (msg.what == MSG_UPDATE_CHANNELS) { + channelDataManager.handleUpdateChannels(); + } + } + } + + private class SendChannelStatusRunnable implements Runnable { + @Override + public void run() { + int browsableChannelCount = 0; + for (Channel channel : mChannels) { + if (channel.isBrowsable()) { + ++browsableChannelCount; + } + } + mTracker.sendChannelCount(browsableChannelCount, mChannels.size()); + } + } } diff --git a/src/com/android/tv/data/ChannelLogoFetcher.java b/src/com/android/tv/data/ChannelLogoFetcher.java index 2f75cd9f..166b1d87 100644 --- a/src/com/android/tv/data/ChannelLogoFetcher.java +++ b/src/com/android/tv/data/ChannelLogoFetcher.java @@ -23,6 +23,7 @@ import android.media.tv.TvContract; import android.media.tv.TvContract.Channels; import android.net.Uri; import android.os.AsyncTask; +import android.support.annotation.WorkerThread; import android.text.TextUtils; import android.util.Log; @@ -208,11 +209,11 @@ public class ChannelLogoFetcher { } // 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[] splittedNames = channelName.split(NAME_SEPARATOR_FOR_DB); - if (splittedNames.length > 1) { + String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_DB); + if (splitNames.length > 1) { StringBuilder sb = new StringBuilder(); - for (String splittedName : splittedNames) { - sb.append(splittedName); + for (String splitName : splitNames) { + sb.append(splitName); } logoUri = channelNameLogoUriMap.get(sb.toString()); if (DEBUG && TextUtils.isEmpty(logoUri)) { @@ -220,10 +221,10 @@ public class ChannelLogoFetcher { } } if (TextUtils.isEmpty(logoUri) - && splittedNames[0].length() != channelName.length()) { - logoUri = channelNameLogoUriMap.get(splittedNames[0]); + && splitNames[0].length() != channelName.length()) { + logoUri = channelNameLogoUriMap.get(splitNames[0]); if (DEBUG && TextUtils.isEmpty(logoUri)) { - Log.d(TAG, "Can't find a logo URI for channel '" + splittedNames[0] + Log.d(TAG, "Can't find a logo URI for channel '" + splitNames[0] + "'"); } } @@ -262,6 +263,7 @@ public class ChannelLogoFetcher { return null; } + @WorkerThread private Map readTmsFile(Context context, String fileName) throws IOException { try (BufferedReader reader = new BufferedReader(new InputStreamReader( @@ -295,9 +297,9 @@ public class ChannelLogoFetcher { // Find the candidate names. // If the name is like "W05AAD (W05AA-D)", then split the names into "W05AAD" and // "W05AA-D" - String[] splittedNames = channelName.split(NAME_SEPARATOR_FOR_TMS); - if (splittedNames.length > 1) { - for (String name : splittedNames) { + 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); diff --git a/src/com/android/tv/data/DisplayMode.java b/src/com/android/tv/data/DisplayMode.java index 7f76dde6..ccba5480 100644 --- a/src/com/android/tv/data/DisplayMode.java +++ b/src/com/android/tv/data/DisplayMode.java @@ -28,6 +28,11 @@ public class DisplayMode { public static final int MODE_ZOOM = 2; public static final int SIZE_OF_RATIO_TYPES = MODE_ZOOM + 1; + /** + * Constant to indicate that any mode is not set yet. + */ + public static final int MODE_NOT_DEFINED = -1; + private DisplayMode() { } public static String getLabel(int mode, Context context) { diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/Program.java index 7f2f73a5..82638e14 100644 --- a/src/com/android/tv/data/Program.java +++ b/src/com/android/tv/data/Program.java @@ -42,7 +42,7 @@ public final class Program implements Comparable { private static final String TAG = "Program"; public static final String[] PROJECTION = { - // Columns should match what is read in Program.fromCursor() + // Columns must match what is read in Program.fromCursor() TvContract.Programs.COLUMN_CHANNEL_ID, TvContract.Programs.COLUMN_TITLE, TvContract.Programs.COLUMN_EPISODE_TITLE, @@ -59,6 +59,32 @@ public final class Program implements Comparable { TvContract.Programs.COLUMN_VIDEO_HEIGHT }; + /** + * Creates {@code Program} object from cursor. + * + *

The query that created the cursor MUST use {@link #PROJECTION}. + */ + public static Program fromCursor(Cursor cursor) { + // Columns read must match the order of match {@link #PROJECTION} + Builder builder = new Builder(); + int index = 0; + builder.setChannelId(cursor.getLong(index++)); + builder.setTitle(cursor.getString(index++)); + builder.setEpisodeTitle(cursor.getString(index++)); + builder.setSeasonNumber(cursor.getInt(index++)); + builder.setEpisodeNumber(cursor.getInt(index++)); + builder.setDescription(cursor.getString(index++)); + builder.setPosterArtUri(cursor.getString(index++)); + builder.setThumbnailUri(cursor.getString(index++)); + builder.setCanonicalGenres(cursor.getString(index++)); + builder.setContentRatings(Utils.stringToContentRatings(cursor.getString(index++))); + builder.setStartTimeUtcMillis(cursor.getLong(index++)); + builder.setEndTimeUtcMillis(cursor.getLong(index++)); + builder.setVideoWidth((int) cursor.getLong(index++)); + builder.setVideoHeight((int) cursor.getLong(index++)); + return builder.build(); + } + private long mChannelId; private String mTitle; private String mEpisodeTitle; @@ -266,82 +292,6 @@ public final class Program implements Comparable { mContentRatings = other.mContentRatings; } - public static Program fromCursor(Cursor cursor) { - // Columns read here should match Program.PROJECTION - - Builder builder = new Builder(); - int index = cursor.getColumnIndex(TvContract.Programs.COLUMN_CHANNEL_ID); - if (index >= 0) { - builder.setChannelId(cursor.getLong(index)); - } - - index = cursor.getColumnIndex(TvContract.Programs.COLUMN_TITLE); - if (index >= 0) { - builder.setTitle(cursor.getString(index)); - } - - index = cursor.getColumnIndex(TvContract.Programs.COLUMN_EPISODE_TITLE); - if (index >= 0) { - builder.setEpisodeTitle(cursor.getString(index)); - } - - index = cursor.getColumnIndex(TvContract.Programs.COLUMN_SEASON_NUMBER); - if(index >= 0) { - builder.setSeasonNumber(cursor.getInt(index)); - } - - index = cursor.getColumnIndex(TvContract.Programs.COLUMN_EPISODE_NUMBER); - if(index >= 0) { - builder.setEpisodeNumber(cursor.getInt(index)); - } - - index = cursor.getColumnIndex(TvContract.Programs.COLUMN_SHORT_DESCRIPTION); - if (index >= 0) { - builder.setDescription(cursor.getString(index)); - } - - index = cursor.getColumnIndex(TvContract.Programs.COLUMN_POSTER_ART_URI); - if (index >= 0) { - builder.setPosterArtUri(cursor.getString(index)); - } - - index = cursor.getColumnIndex(TvContract.Programs.COLUMN_THUMBNAIL_URI); - if (index >= 0) { - builder.setThumbnailUri(cursor.getString(index)); - } - - index = cursor.getColumnIndex(TvContract.Programs.COLUMN_CANONICAL_GENRE); - if (index >= 0) { - builder.setCanonicalGenres(cursor.getString(index)); - } - - index = cursor.getColumnIndex(TvContract.Programs.COLUMN_CONTENT_RATING); - if (index >= 0) { - builder.setContentRatings(Utils.stringToContentRatings(cursor.getString(index))); - } - - index = cursor.getColumnIndex(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS); - if (index >= 0) { - builder.setStartTimeUtcMillis(cursor.getLong(index)); - } - - index = cursor.getColumnIndex(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS); - if (index >= 0) { - builder.setEndTimeUtcMillis(cursor.getLong(index)); - } - - index = cursor.getColumnIndex(TvContract.Programs.COLUMN_VIDEO_WIDTH); - if (index >= 0) { - builder.setVideoWidth((int) cursor.getLong(index)); - } - index = cursor.getColumnIndex(TvContract.Programs.COLUMN_VIDEO_HEIGHT); - if (index >= 0) { - builder.setVideoHeight((int) cursor.getLong(index)); - } - - return builder.build(); - } - public static final class Builder { private final Program mProgram; diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java index 80733bc5..c12e7094 100644 --- a/src/com/android/tv/data/ProgramDataManager.java +++ b/src/com/android/tv/data/ProgramDataManager.java @@ -31,6 +31,7 @@ import android.util.Log; import android.util.LongSparseArray; import android.util.LruCache; +import com.android.tv.BuildConfig; import com.android.tv.MainActivity.MemoryManageable; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.Clock; @@ -421,6 +422,7 @@ public class ProgramDataManager implements MemoryManageable { continue; } while (c.moveToNext()) { + int duplicateCount = 0; if (isCancelled()) { if (DEBUG) { Log.d(TAG, "ProgramsPrefetchTask canceled."); @@ -429,6 +431,7 @@ public class ProgramDataManager implements MemoryManageable { } Program program = Program.fromCursor(c); if (isDuplicateProgram(program, lastReadProgram)) { + duplicateCount++; continue; } else { lastReadProgram = program; @@ -439,6 +442,9 @@ public class ProgramDataManager implements MemoryManageable { programMap.put(program.getChannelId(), programs); } programs.add(program); + if (duplicateCount > 0) { + Log.w(TAG, "Found " + duplicateCount + " duplicate programs"); + } } mSuccess = true; break; @@ -498,6 +504,7 @@ public class ProgramDataManager implements MemoryManageable { public List onQuery(Cursor c) { final List programs = new ArrayList<>(); if (c != null) { + int duplicateCount = 0; Program lastReadProgram = null; while (c.moveToNext()) { if (isCancelled()) { @@ -505,21 +512,23 @@ public class ProgramDataManager implements MemoryManageable { } Program program = Program.fromCursor(c); if (isDuplicateProgram(program, lastReadProgram)) { + duplicateCount++; continue; } else { lastReadProgram = program; } programs.add(program); } + if (duplicateCount > 0) { + Log.w(TAG, "Found " + duplicateCount + " duplicate programs"); + } } return programs; } @Override protected void onPostExecute(List programs) { - if (DEBUG) { - Log.d(TAG, "ProgramsUpdateTask done"); - } + if (DEBUG) Log.d(TAG, "ProgramsUpdateTask done"); mProgramsUpdateTask = null; if (programs == null) { return; @@ -674,7 +683,7 @@ public class ProgramDataManager implements MemoryManageable { boolean isDuplicate = p1.getChannelId() == p2.getChannelId() && p1.getStartTimeUtcMillis() == p2.getStartTimeUtcMillis() && p1.getEndTimeUtcMillis() == p2.getEndTimeUtcMillis(); - if (isDuplicate) { + if (BuildConfig.ENG && isDuplicate) { Log.w(TAG, "Duplicate programs detected! - \"" + p1.getTitle() + "\" and \"" + p2.getTitle() + "\""); } diff --git a/src/com/android/tv/data/StreamInfo.java b/src/com/android/tv/data/StreamInfo.java index af5b4e3d..04f8258a 100644 --- a/src/com/android/tv/data/StreamInfo.java +++ b/src/com/android/tv/data/StreamInfo.java @@ -16,8 +16,6 @@ package com.android.tv.data; -import android.media.tv.TvInputInfo; - public interface StreamInfo { int VIDEO_DEFINITION_LEVEL_UNKNOWN = 0; int VIDEO_DEFINITION_LEVEL_SD = 1; diff --git a/src/com/android/tv/dialog/PinDialogFragment.java b/src/com/android/tv/dialog/PinDialogFragment.java index 84464461..3952bb0b 100644 --- a/src/com/android/tv/dialog/PinDialogFragment.java +++ b/src/com/android/tv/dialog/PinDialogFragment.java @@ -120,8 +120,8 @@ public class PinDialogFragment extends SafeDismissDialogFragment { setStyle(STYLE_NO_TITLE, 0); mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); mDisablePinUntil = TvSettings.getDisablePinUntil(getActivity()); - if (ActivityManager.isRunningInTestHarness()) { - // Skip PIN dialog half the time. + if (ActivityManager.isUserAMonkey()) { + // Skip PIN dialog half the time for monkeys if (Math.random() < 0.5) { exit(PIN_DIALOG_RESULT_SUCCESS); } diff --git a/src/com/android/tv/dialog/SafeDismissDialogFragment.java b/src/com/android/tv/dialog/SafeDismissDialogFragment.java index c734653c..bd1c55a6 100644 --- a/src/com/android/tv/dialog/SafeDismissDialogFragment.java +++ b/src/com/android/tv/dialog/SafeDismissDialogFragment.java @@ -52,7 +52,6 @@ public abstract class SafeDismissDialogFragment extends DialogFragment if (mDismissPending) { mDismissPending = false; dismiss(); - return; } } diff --git a/src/com/android/tv/guide/ProgramGrid.java b/src/com/android/tv/guide/ProgramGrid.java index 27c8a0c4..99da84b0 100644 --- a/src/com/android/tv/guide/ProgramGrid.java +++ b/src/com/android/tv/guide/ProgramGrid.java @@ -16,11 +16,16 @@ package com.android.tv.guide; +import com.android.tv.R; +import com.android.tv.ui.OnRepeatedKeyInterceptListener; + import android.content.Context; +import android.content.res.Resources; import android.graphics.Rect; import android.support.v17.leanback.widget.VerticalGridView; import android.util.AttributeSet; import android.util.Log; +import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; @@ -69,12 +74,17 @@ public class ProgramGrid extends VerticalGridView { private int mFocusRangeLeft; private int mFocusRangeRight; + private final int mRowHeight; + private final int mDetailHeight; + private final int mSelectionRow; // Row that is focused + private View mLastFocusedView; private final Rect mTempRect = new Rect(); private boolean mKeepCurrentProgram; private ChildFocusListener mChildFocusListener; + private OnRepeatedKeyInterceptListener mOnRepeatedKeyInterceptListener; interface ChildFocusListener { /** @@ -103,6 +113,13 @@ public class ProgramGrid extends VerticalGridView { // E.g. when scrolling horizontally we would have to update rows above and below the current // view port even though they are not visible. setItemViewCacheSize(0); + + Resources res = context.getResources(); + mRowHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_item_row_height); + mDetailHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_detail_height); + mSelectionRow = res.getInteger(R.integer.program_guide_selection_row); + mOnRepeatedKeyInterceptListener = new OnRepeatedKeyInterceptListener(this); + setOnKeyInterceptListener(mOnRepeatedKeyInterceptListener); } /** @@ -326,6 +343,25 @@ public class ProgramGrid extends VerticalGridView { return super.onRequestFocusInDescendants(direction, previouslyFocusedRect); } + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + // It is required to properly handle OnRepeatedKeyInterceptListener. If the focused + // item's are at the almost end of screen, focus change to the next item doesn't work. + // It restricts that a focus item's position cannot be too far from the desired position. + View focusedView = findFocus(); + if (focusedView != null && mOnRepeatedKeyInterceptListener.isFocusAccelerated()) { + int[] location = new int[2]; + getLocationOnScreen(location); + int[] focusedLocation = new int[2]; + focusedView.getLocationOnScreen(focusedLocation); + int y = focusedLocation[1] - location[1]; + int minY = (mSelectionRow - 1) * mRowHeight; + if (y < minY) scrollBy(0, y - minY); + int maxY = (mSelectionRow + 1) * mRowHeight + mDetailHeight; + if (y > maxY) scrollBy(0, y - maxY); + } + } + private static void findFocusables(View v, ArrayList outFocusable) { if (v.isFocusable()) { outFocusable.add(v); diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java index 03bda694..468f10e0 100644 --- a/src/com/android/tv/guide/ProgramGuide.java +++ b/src/com/android/tv/guide/ProgramGuide.java @@ -30,6 +30,7 @@ import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.v17.leanback.widget.OnChildSelectedListener; import android.support.v17.leanback.widget.SearchOrbView; import android.support.v17.leanback.widget.VerticalGridView; @@ -37,6 +38,7 @@ import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; import android.view.View.MeasureSpec; +import android.view.View.OnScrollChangeListener; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewTreeObserver; @@ -46,10 +48,12 @@ import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.Tracker; +import com.android.tv.common.WeakHandler; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.GenreItems; import com.android.tv.data.ProgramDataManager; import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; +import com.android.tv.ui.OnRepeatedKeyInterceptListener; import com.android.tv.util.SystemProperties; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -81,7 +85,6 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { private static final int MSG_PROGRAM_TABLE_FADE_IN_ANIM = 1000; - private static final int SELECTION_ROW = 2; // Row that is focused private static final String SCREEN_NAME = "EPG"; private final MainActivity mActivity; @@ -96,6 +99,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { private final long mViewPortMillis; private final int mRowHeight; private final int mDetailHeight; + private final int mSelectionRow; // Row that is focused private final int mTableFadeAnimDuration; private final int mAnimationDuration; private final int mDetailPadding; @@ -134,14 +138,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { private boolean mTimelineAnimation; private int mLastRequestedGenreId = GenreItems.ID_ALL_CHANNELS; private boolean mIsDuringResetRowSelection; - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - if (msg.what == MSG_PROGRAM_TABLE_FADE_IN_ANIM) { - mProgramTableFadeInAnimator.start(); - } - } - }; + private final Handler mHandler = new ProgramGuideHandler(this); private final Runnable mHideRunnable = new Runnable() { @Override @@ -150,6 +147,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { } }; private final long mShowDurationMillis; + private ViewTreeObserver.OnGlobalLayoutListener mOnLayoutListenerForShow; private final ProgramManagerListener mProgramManagerListener = new ProgramManagerListener(); @@ -189,6 +187,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mRowHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_item_row_height); mDetailHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_detail_height); + mSelectionRow = res.getInteger(R.integer.program_guide_selection_row); mTableFadeAnimDuration = res.getInteger(R.integer.program_guide_table_detail_fade_anim_duration); mShowDurationMillis = res.getInteger(R.integer.program_guide_show_duration); @@ -307,7 +306,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { } }); mGrid.setFocusScrollStrategy(ProgramGrid.FOCUS_SCROLL_ALIGNED); - mGrid.setWindowAlignmentOffset(SELECTION_ROW * mRowHeight); + mGrid.setWindowAlignmentOffset(mSelectionRow * mRowHeight); mGrid.setWindowAlignmentOffsetPercent(ProgramGrid.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); mGrid.setItemAlignmentOffset(0); mGrid.setItemAlignmentOffsetPercent(ProgramGrid.ITEM_ALIGN_OFFSET_PERCENT_DISABLED); @@ -353,7 +352,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mHideAnimatorFull.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - mContainer.setVisibility(View.INVISIBLE); + mContainer.setVisibility(View.GONE); } }); mHideAnimatorPartial = createAnimator( @@ -363,7 +362,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mHideAnimatorPartial.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - mContainer.setVisibility(View.INVISIBLE); + mContainer.setVisibility(View.GONE); } }); @@ -405,7 +404,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { @Override public void onRequestChildFocus(View oldFocus, View newFocus) { if (oldFocus != null && newFocus != null) { - int selectionRowOffset = SELECTION_ROW * mRowHeight; + int selectionRowOffset = mSelectionRow * mRowHeight; if (oldFocus.getTop() < newFocus.getTop()) { // Selection moves downwards // Adjust scroll offset to be at the bottom of the target row and to expand up. This @@ -459,8 +458,12 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { /** * Show the program guide. This reveals the side panel, and the program guide table is shown * partially. + * + *

Note: the animation which starts together with ProgramGuide showing animation needs to + * be initiated in {@code runnableAfterAnimatorReady}. If the animation starts together + * with show(), the animation may drop some frames. */ - public void show() { + public void show(final Runnable runnableAfterAnimatorReady) { if (mContainer.getVisibility() == View.VISIBLE) { return; } @@ -475,9 +478,8 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mStartUtcTime = Utils.floorTime( System.currentTimeMillis() - MIN_DURATION_FROM_START_TIME_TO_CURRENT_TIME, HALF_HOUR_IN_MILLIS); - mProgramManager.setInitialTimeRange(mStartUtcTime, mStartUtcTime + mViewPortMillis); + mProgramManager.updateInitialTimeRange(mStartUtcTime, mStartUtcTime + mViewPortMillis); mProgramManager.addListener(mProgramManagerListener); - mProgramManager.buildGenreFilters(); mLastRequestedGenreId = GenreItems.ID_ALL_CHANNELS; mTimeListAdapter.update(mStartUtcTime); mTimelineRow.resetScroll(); @@ -490,23 +492,48 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { } mContainer.setVisibility(View.VISIBLE); + positionCurrentTimeIndicator(); mSidePanelGridView.setSelectedPosition(0); - mHandler.post(new Runnable() { + if (DEBUG) { + Log.d(TAG, "show()"); + } + mOnLayoutListenerForShow = new ViewTreeObserver.OnGlobalLayoutListener() { @Override - public void run() { - // setVisibility is not immediately applied. In order to start animation after - // making it visible, we post mShowAnimatorXXX.start() instead of calling - // mShowAnimatorXXX.start() in show(). + public void onGlobalLayout() { + mContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this); + mTable.setLayerType(View.LAYER_TYPE_HARDWARE, null); + mSidePanelGridView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + mTable.buildLayer(); + mSidePanelGridView.buildLayer(); + mOnLayoutListenerForShow = null; + mTimelineAnimation = true; + // Make sure that time indicator update starts after animation is finished. + startCurrentTimeIndicator(TIME_INDICATOR_UPDATE_FREQUENCY); + if (DEBUG) { + mContainer.getViewTreeObserver().addOnDrawListener( + new ViewTreeObserver.OnDrawListener() { + long time = System.currentTimeMillis(); + int count = 0; + @Override + public void onDraw() { + long curtime = System.currentTimeMillis(); + Log.d(TAG, "onDraw " + count++ + " " + (curtime - time) + "ms"); + time = curtime; + if (count > 10) { + mContainer.getViewTreeObserver().removeOnDrawListener(this); + } + } + }); + } + runnableAfterAnimatorReady.run(); if (mShowGuidePartial) { mShowAnimatorPartial.start(); } else { mShowAnimatorFull.start(); } } - }); - - mTimelineAnimation = true; - startCurrentTimeIndicator(); + }; + mContainer.getViewTreeObserver().addOnGlobalLayoutListener(mOnLayoutListenerForShow); scheduleHide(); } @@ -517,6 +544,10 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { if (!isActive()) { return; } + if (mOnLayoutListenerForShow != null) { + mContainer.getViewTreeObserver().removeOnGlobalLayoutListener(mOnLayoutListenerForShow); + mOnLayoutListenerForShow = null; + } mTracker.sendHideEpg(mVisibleDuration.reset()); cancelHide(); mProgramManager.programGuideVisibilityChanged(false); @@ -631,8 +662,8 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mProgramTableFadeOutAnimator.start(); } - private void startCurrentTimeIndicator() { - mHandler.post(mUpdateTimeIndicator); + private void startCurrentTimeIndicator(long initialDelay) { + mHandler.postDelayed(mUpdateTimeIndicator, initialDelay); } private void stopCurrentTimeIndicator() { @@ -896,4 +927,17 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mTimelineRow.scrollTo(scrollOffset, mTimelineAnimation); } } + + private static class ProgramGuideHandler extends WeakHandler { + public ProgramGuideHandler(ProgramGuide ref) { + super(ref); + } + + @Override + public void handleMessage(Message msg, @NonNull ProgramGuide programGuide) { + if (msg.what == MSG_PROGRAM_TABLE_FADE_IN_ANIM) { + programGuide.mProgramTableFadeInAnimator.start(); + } + } + } } diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java index 7d3c0190..1babc255 100644 --- a/src/com/android/tv/guide/ProgramItemView.java +++ b/src/com/android/tv/guide/ProgramItemView.java @@ -58,7 +58,6 @@ public class ProgramItemView extends TextView { private static final int[] STATE_TOO_WIDE = { R.attr.state_program_too_wide }; private static int sVisibleThreshold; - private static int sMinProgramDisplayDurationPixels; private static int sItemPadding; private static TextAppearanceSpan sProgramTitleStyle; private static TextAppearanceSpan sGrayedOutProgramTitleStyle; @@ -67,6 +66,7 @@ public class ProgramItemView extends TextView { private TableEntry mTableEntry; private int mMaxWidthForRipple; + private int mTextWidth; // If set this flag disables requests to re-layout the parent view as a result of changing // this view, improving performance. This also prevents the parent view to lose child focus @@ -148,8 +148,6 @@ public class ProgramItemView extends TextView { sVisibleThreshold = res.getDimensionPixelOffset( R.dimen.program_guide_table_item_visible_threshold); - sMinProgramDisplayDurationPixels = res.getDimensionPixelOffset( - R.dimen.program_guide_table_item_min_program_display_width); sItemPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_item_padding); @@ -256,7 +254,8 @@ public class ProgramItemView extends TextView { } setText(description); } - + measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + mTextWidth = getMeasuredWidth() - getPaddingStart() - getPaddingEnd(); int start = GuideUtils.convertMillisToPixel(entry.entryStartUtcMillis); int guideStart = GuideUtils.convertMillisToPixel(programManager.getFromUtcMillis()); layoutVisibleArea(guideStart - start); @@ -280,8 +279,9 @@ public class ProgramItemView extends TextView { public void layoutVisibleArea(int offset) { int width = mTableEntry.getWidth(); int startPadding = Math.max(0, offset); - if (startPadding > 0 && width - startPadding < sMinProgramDisplayDurationPixels) { - startPadding = Math.max(0, width - sMinProgramDisplayDurationPixels); + int minWidth = Math.min(width, mTextWidth + 2 * sItemPadding); + if (startPadding > 0 && width - startPadding < minWidth) { + startPadding = Math.max(0, width - minWidth); } if (startPadding + sItemPadding != getPaddingStart()) { diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java index 3310e33e..fde903d1 100644 --- a/src/com/android/tv/guide/ProgramManager.java +++ b/src/com/android/tv/guide/ProgramManager.java @@ -175,17 +175,17 @@ public class ProgramManager { mChannelDataManager.addListener(new ChannelDataManager.Listener() { @Override public void onLoadFinished() { - updateChannels(true); + updateChannels(true, false); } @Override public void onChannelListUpdated() { - updateChannels(true); + updateChannels(true, false); } @Override public void onChannelBrowsableChanged() { - updateChannels(true); + updateChannels(true, false); } }); @@ -300,7 +300,7 @@ public class ProgramManager { // Note that This can be happens only if program guide isn't shown // because an user has to select channels as browsable through UI. - private void updateChannels(boolean notify) { + private void updateChannels(boolean notify, boolean clearPreviousTableEntries) { if (DEBUG) Log.d(TAG, "updateChannels"); mChannels = mChannelDataManager.getBrowsableChannelList(); mSelectedGenreId = GenreItems.ID_ALL_CHANNELS; @@ -308,7 +308,7 @@ public class ProgramManager { if (notify) { notifyChannelsUpdated(); } - updateTableEntries(notify, false); + updateTableEntries(notify, clearPreviousTableEntries); } private void updateTableEntries(boolean notify, boolean clear) { @@ -404,19 +404,16 @@ public class ProgramManager { } /** - * Set the initial time range to manage. + * Update the initial time range to manage. It updates program entries and genre as well. */ - public void setInitialTimeRange(long startUtcMillis, long endUtcMillis) { + public void updateInitialTimeRange(long startUtcMillis, long endUtcMillis) { mStartUtcMillis = startUtcMillis; if (endUtcMillis > mEndUtcMillis) { mEndUtcMillis = endUtcMillis; } - updateChannels(true); mProgramDataManager.setPrefetchTimeRange(mStartUtcMillis); - - // Need to clear when the UI starts. - updateTableEntries(true, true); + updateChannels(true, true); setTimeRange(startUtcMillis, endUtcMillis); } @@ -580,7 +577,7 @@ public class ProgramManager { } /** - * Returns the start time set by {@link #setInitialTimeRange}. + * Returns the start time set by {@link #updateInitialTimeRange}. */ public long getStartTime() { return mStartUtcMillis; diff --git a/src/com/android/tv/menu/AppLinkCardView.java b/src/com/android/tv/menu/AppLinkCardView.java index cad63ced..19f22cc1 100644 --- a/src/com/android/tv/menu/AppLinkCardView.java +++ b/src/com/android/tv/menu/AppLinkCardView.java @@ -274,7 +274,9 @@ public class AppLinkCardView extends BaseCardView implements Channel.Lo banner.setBounds(0, 0, mCardImageWidth, mCardImageHeight); banner.draw(canvas); mImageView.setImageDrawable(banner); - extractAndSetMetaViewBackgroundColor(bitmap); + if (mChannel.getAppLinkColor() == 0) { + extractAndSetMetaViewBackgroundColor(bitmap); + } } } diff --git a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java index 1dca6834..b008fa65 100644 --- a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java +++ b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java @@ -19,12 +19,15 @@ package com.android.tv.menu; import android.content.Context; import android.os.Handler; import android.os.Message; +import android.support.annotation.NonNull; import android.util.Log; import com.android.tv.R; +import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.data.ProgramDataManager; +import com.android.tv.util.Utils; import java.util.List; @@ -34,27 +37,18 @@ import java.util.List; public class ChannelsPosterPrefetcher { private static final String TAG = "PosterPrefetcher"; private static final boolean DEBUG = false; + private static final int MSG_PREFETCH_IMAGE = 1000; + private static final int ONDEMAND_POSTER_PREFETCH_DELAY_MILLIS = 500; // 500 milliseconds private final ProgramDataManager mProgramDataManager; private final ChannelsRowAdapter mChannelsAdapter; private final int mPosterArtWidth; private final int mPosterArtHeight; private final Context mContext; + private final Handler mHandler = new PrefetchHandler(this); - private static final int MSG_PREFETCH_IMAGE = 1000; + private boolean isCanceled; - private static final int ONDEMAND_POSTER_PREFETCH_DELAY_MILLIS = 500; // 500 milliseconds - - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_PREFETCH_IMAGE: - doPrefetchImages(); - break; - } - } - }; /** * Create {@link ChannelsPosterPrefetcher} object with given parameters. @@ -67,13 +61,17 @@ public class ChannelsPosterPrefetcher { R.dimen.card_image_layout_width); mPosterArtHeight = context.getResources().getDimensionPixelSize( R.dimen.card_image_layout_height); - mContext = context; + mContext = context.getApplicationContext(); } /** * Start prefetching of program poster art of recommendation. */ public void prefetch() { + if (isCanceled) { + Utils.engThrowElseWarn(TAG, "Prefetch called after cancel was called."); + return; + } if (DEBUG) { Log.d(TAG, "startPrefetching()"); } @@ -86,6 +84,14 @@ public class ChannelsPosterPrefetcher { mHandler.obtainMessage(MSG_PREFETCH_IMAGE), ONDEMAND_POSTER_PREFETCH_DELAY_MILLIS); } + /** + * Cancels pending and current prefetch requests. + */ + public void cancel() { + isCanceled = true; + mHandler.removeCallbacksAndMessages(null); + } + private void doPrefetchImages() { if (DEBUG) { Log.d(TAG, "doPrefetchImages()"); @@ -94,6 +100,9 @@ public class ChannelsPosterPrefetcher { List channelList = mChannelsAdapter.getItemList(); if (channelList != null) { for (Channel channel : channelList) { + if (isCanceled) { + return; + } if (!Channel.isValid(channel)) { continue; } @@ -106,4 +115,19 @@ public class ChannelsPosterPrefetcher { } } } + + private static class PrefetchHandler extends WeakHandler { + public PrefetchHandler(ChannelsPosterPrefetcher ref) { + super(ref); + } + + @Override + public void handleMessage(Message msg, @NonNull ChannelsPosterPrefetcher prefetcher) { + switch (msg.what) { + case MSG_PREFETCH_IMAGE: + prefetcher.doPrefetchImages(); + break; + } + } + } } diff --git a/src/com/android/tv/menu/ChannelsRow.java b/src/com/android/tv/menu/ChannelsRow.java index f08cbd57..dedf0993 100644 --- a/src/com/android/tv/menu/ChannelsRow.java +++ b/src/com/android/tv/menu/ChannelsRow.java @@ -19,7 +19,7 @@ package com.android.tv.menu; import android.content.Context; import com.android.tv.R; -import com.android.tv.common.TvCommonConstants; +import com.android.tv.data.ProgramDataManager; import com.android.tv.recommendation.RecentChannelEvaluator; import com.android.tv.recommendation.Recommender; @@ -33,12 +33,8 @@ public class ChannelsRow extends ItemListRow { private ChannelsRowAdapter mChannelsAdapter; private ChannelsPosterPrefetcher mChannelsPosterPrefetcher; - public ChannelsRow(Context context) { - super(context, - TvCommonConstants.IS_MNC_OR_HIGHER - ? R.string.menu_title_channels : R.string.menu_title_channels_legacy, - R.dimen.card_layout_height, - null); + public ChannelsRow(Context context, Menu menu, ProgramDataManager programDataManager) { + super(context, menu, R.string.menu_title_channels, R.dimen.card_layout_height, null); mTvRecommendation = new Recommender(getContext(), new Recommender.Listener() { @Override public void onRecommenderReady() { @@ -56,21 +52,25 @@ public class ChannelsRow extends ItemListRow { mChannelsAdapter = new ChannelsRowAdapter(context, mTvRecommendation, MIN_COUNT_FOR_RECENT_CHANNELS, MAX_COUNT_FOR_RECENT_CHANNELS); setAdapter(mChannelsAdapter); - mChannelsPosterPrefetcher = new ChannelsPosterPrefetcher(context, - getMainActivity().getProgramDataManager(), mChannelsAdapter); + mChannelsPosterPrefetcher = new ChannelsPosterPrefetcher(context, programDataManager, + mChannelsAdapter); } @Override public void release() { super.release(); - mTvRecommendation.release(); - mTvRecommendation = null; + if (mTvRecommendation != null) { + mTvRecommendation.release(); + mTvRecommendation = null; + } + mChannelsPosterPrefetcher.cancel(); } /** * Handle the update event of the recent channel. */ - public void onRecentChannelUpdated() { + @Override + public void onRecentChannelsChanged() { mChannelsPosterPrefetcher.prefetch(); } diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java index 8c76912a..8190c976 100644 --- a/src/com/android/tv/menu/ChannelsRowAdapter.java +++ b/src/com/android/tv/menu/ChannelsRowAdapter.java @@ -18,13 +18,13 @@ package com.android.tv.menu; import android.content.Context; import android.content.Intent; +import android.os.Build; import android.view.View; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.analytics.Tracker; -import com.android.tv.common.TvCommonConstants; import com.android.tv.data.Channel; import com.android.tv.recommendation.Recommender; import com.android.tv.util.SetupUtils; @@ -151,7 +151,7 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter if (mShowSetupCard) { channelList.add(dummyChannel); } - if (TvCommonConstants.IS_MNC_OR_HIGHER) { + if (Build.VERSION.SDK_INT >= 23) { Channel currentChannel = ((MainActivity) mContext).getCurrentChannel(); mShowAppLinkCard = currentChannel != null && currentChannel.getAppLinkType(mContext) != Channel.APP_LINK_TYPE_NONE; diff --git a/src/com/android/tv/menu/IMenuView.java b/src/com/android/tv/menu/IMenuView.java new file mode 100644 index 00000000..99fb4126 --- /dev/null +++ b/src/com/android/tv/menu/IMenuView.java @@ -0,0 +1,60 @@ +/* + * 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 com.android.tv.menu.Menu.MenuShowReason; + +import java.util.List; + +/** + * An base interface for menu view. + */ +public interface IMenuView { + /** + * Sets menu rows. + */ + void setMenuRows(List menuRows); + + /** + * Shows the main menu. + * + *

The inherited class should show the menu and select the row corresponding to + * {@code rowIdToSelect}. If the menu is already visible, change the current selection to the + * given row. + * + * @param reason A reason why this is called. See {@link MenuShowReason}. + * @param rowIdToSelect An ID of the row which corresponds to the {@code reason}. + */ + void onShow(@MenuShowReason int reason, String rowIdToSelect, Runnable runnableAfterShow); + + /** + * Hides the main menu + */ + void onHide(); + + /** + * Updates the menu contents. + * + *

Returns <@code true> if the contents have been changed, otherwise {@code false}. + */ + boolean update(boolean menuActive); + + /** + * Checks if the menu view is visible or not. + */ + boolean isVisible(); +} diff --git a/src/com/android/tv/menu/ItemListRow.java b/src/com/android/tv/menu/ItemListRow.java index ab634783..faa611fa 100644 --- a/src/com/android/tv/menu/ItemListRow.java +++ b/src/com/android/tv/menu/ItemListRow.java @@ -30,14 +30,14 @@ import com.android.tv.menu.ItemListRowView.ItemListAdapter; public class ItemListRow extends MenuRow { private ItemListAdapter mAdapter; - public ItemListRow(Context context, int titleResId, int itemHeightResId, + public ItemListRow(Context context, Menu menu, int titleResId, int itemHeightResId, ItemListAdapter adapter) { - this(context, context.getString(titleResId), itemHeightResId, adapter); + this(context, menu, context.getString(titleResId), itemHeightResId, adapter); } - public ItemListRow(Context context, String title, int itemHeightResId, + public ItemListRow(Context context, Menu menu, String title, int itemHeightResId, ItemListAdapter adapter) { - super(context, title, itemHeightResId); + super(context, menu, title, itemHeightResId); mAdapter = adapter; } diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java new file mode 100644 index 00000000..323ce9c5 --- /dev/null +++ b/src/com/android/tv/menu/Menu.java @@ -0,0 +1,330 @@ +/* + * 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.animation.Animator; +import android.animation.AnimatorInflater; +import android.animation.AnimatorListenerAdapter; +import android.content.Context; +import android.content.res.Resources; +import android.os.Looper; +import android.os.Message; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.util.Log; + +import com.android.tv.ChannelTuner; +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.analytics.DurationTimer; +import com.android.tv.analytics.Tracker; +import com.android.tv.common.WeakHandler; +import com.android.tv.data.Channel; +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.util.Utils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; + +/** + * A class which controls the menu. + */ +public class Menu { + private static final String TAG = "Menu"; + private static final boolean DEBUG = false; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({REASON_NONE, REASON_GUIDE, REASON_PLAY_CONTROLS_PLAY, REASON_PLAY_CONTROLS_PAUSE, + REASON_PLAY_CONTROLS_PLAY_PAUSE, REASON_PLAY_CONTROLS_REWIND, + REASON_PLAY_CONTROLS_FAST_FORWARD, REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS, + REASON_PLAY_CONTROLS_JUMP_TO_NEXT}) + public @interface MenuShowReason {} + public static final int REASON_NONE = 0; + public static final int REASON_GUIDE = 1; + public static final int REASON_PLAY_CONTROLS_PLAY = 2; + public static final int REASON_PLAY_CONTROLS_PAUSE = 3; + public static final int REASON_PLAY_CONTROLS_PLAY_PAUSE = 4; + public static final int REASON_PLAY_CONTROLS_REWIND = 5; + public static final int REASON_PLAY_CONTROLS_FAST_FORWARD = 6; + public static final int REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS = 7; + public static final int REASON_PLAY_CONTROLS_JUMP_TO_NEXT = 8; + + private static final List sRowIdListForReason = new ArrayList<>(); + static { + sRowIdListForReason.add(null); // REASON_NONE + sRowIdListForReason.add(ChannelsRow.ID); // REASON_GUIDE + sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_PLAY + sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_PAUSE + sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_PLAY_PAUSE + sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_REWIND + sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_FAST_FORWARD + sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS + sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_JUMP_TO_NEXT + } + + private static final String SCREEN_NAME = "Menu"; + + private static final int MSG_HIDE_MENU = 1000; + + private final IMenuView mMenuView; + private final Tracker mTracker; + private final DurationTimer mVisibleTimer = new DurationTimer(); + private final long mShowDurationMillis; + private final OnMenuVisibilityChangeListener mOnMenuVisibilityChangeListener; + private final WeakHandler

mHandler = new MenuWeakHandler(this, Looper.getMainLooper()); + + private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() { + @Override + public void onLoadFinished() {} + + @Override + public void onBrowsableChannelListChanged() { + mMenuView.update(isActive()); + } + + @Override + public void onCurrentChannelUnavailable(Channel channel) {} + + @Override + public void onChannelChanged(Channel previousChannel, Channel currentChannel) {} + }; + + private final List mMenuRows = new ArrayList<>(); + private final Animator mShowAnimator; + private final Animator mHideAnimator; + + private ChannelTuner mChannelTuner; + private boolean mKeepVisible; + private boolean mAnimationDisabledForTest; + + /** + * A constructor. + */ + public Menu(Context context, IMenuView menuView, MenuRowFactory menuRowFactory, + OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) { + mMenuView = menuView; + mTracker = ((TvApplication) context.getApplicationContext()).getTracker(); + Resources res = context.getResources(); + mShowDurationMillis = res.getInteger(R.integer.menu_show_duration); + mOnMenuVisibilityChangeListener = onMenuVisibilityChangeListener; + mShowAnimator = AnimatorInflater.loadAnimator(context, R.animator.menu_enter); + mShowAnimator.setTarget(mMenuView); + mHideAnimator = AnimatorInflater.loadAnimator(context, R.animator.menu_exit); + mHideAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mMenuView.onHide(); + } + }); + mHideAnimator.setTarget(mMenuView); + // Build menu rows + addMenuRow(menuRowFactory.createMenuRow(this, PlayControlsRow.class)); + 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); + } + + /** + * 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) { + mChannelTuner.removeListener(mChannelTunerListener); + } + mChannelTuner = channelTuner; + if (mChannelTuner != null) { + mChannelTuner.addListener(mChannelTunerListener); + } + mMenuView.update(isActive()); + } + + private void addMenuRow(MenuRow row) { + if (row != null) { + mMenuRows.add(row); + } + } + + /** + * Call this method to end the lifetime of the menu. + */ + public void release() { + setChannelTuner(null); + for (MenuRow row : mMenuRows) { + row.release(); + } + mHandler.removeCallbacksAndMessages(null); + } + + /** + * Shows the main menu. + * + * @param reason A reason why this is called. See {@link MenuShowReason} + */ + public void show(@MenuShowReason int reason) { + if (DEBUG) Log.d(TAG, "show reason:" + reason); + mTracker.sendShowMenu(); + mVisibleTimer.start(); + mTracker.sendScreenView(SCREEN_NAME); + if (mHideAnimator.isStarted()) { + mHideAnimator.end(); + } + if (mOnMenuVisibilityChangeListener != null) { + mOnMenuVisibilityChangeListener.onMenuVisibilityChange(true); + } + String rowIdToSelect = sRowIdListForReason.get(reason); + mMenuView.onShow(reason, rowIdToSelect, mAnimationDisabledForTest ? null : new Runnable() { + @Override + public void run() { + mShowAnimator.start(); + } + }); + scheduleHide(); + } + + /** + * Closes the menu. + */ + public void hide(boolean withAnimation) { + if (!isActive()) { + return; + } + if (mAnimationDisabledForTest) { + withAnimation = false; + } + mHandler.removeMessages(MSG_HIDE_MENU); + if (withAnimation) { + if (!mHideAnimator.isStarted()) { + mHideAnimator.start(); + } + } else if (mHideAnimator.isStarted()) { + // mMenuView.onHide() is called in AnimatorListener. + mHideAnimator.end(); + } else { + mMenuView.onHide(); + mTracker.sendHideMenu(mVisibleTimer.reset()); + if (mOnMenuVisibilityChangeListener != null) { + mOnMenuVisibilityChangeListener.onMenuVisibilityChange(false); + } + } + } + + /** + * Schedules to hide the menu in some seconds. + */ + public void scheduleHide() { + mHandler.removeMessages(MSG_HIDE_MENU); + if (!mKeepVisible) { + mHandler.sendEmptyMessageDelayed(MSG_HIDE_MENU, mShowDurationMillis); + } + } + + /** + * Called when the caller wants the main menu to be kept visible or not. + * If {@code keepVisible} is set to {@code true}, the hide schedule doesn't close the main menu, + * but calling {@link #hide} still hides it. + * If {@code keepVisible} is set to {@code false}, the hide schedule works as usual. + */ + public void setKeepVisible(boolean keepVisible) { + mKeepVisible = keepVisible; + if (mKeepVisible) { + mHandler.removeMessages(MSG_HIDE_MENU); + } else if (isActive()) { + scheduleHide(); + } + } + + @VisibleForTesting + boolean isHideScheduled() { + return mHandler.hasMessages(MSG_HIDE_MENU); + } + + /** + * Returns {@code true} if the menu is open and not hiding. + */ + public boolean isActive() { + return mMenuView.isVisible() && !mHideAnimator.isStarted(); + } + + /** + * Updates menu contents. + * + *

Returns <@code true> if the contents have been changed, otherwise {@code false}. + */ + public boolean update() { + if (DEBUG) Log.d(TAG, "update main menu"); + return mMenuView.update(isActive()); + } + + /** + * This method is called when channels are changed. + */ + public void onRecentChannelsChanged() { + if (DEBUG) Log.d(TAG, "onRecentChannelsChanged"); + for (MenuRow row : mMenuRows) { + row.onRecentChannelsChanged(); + } + } + + /** + * This method is called when the stream information is changed. + */ + public void onStreamInfoChanged() { + if (DEBUG) Log.d(TAG, "update options row in main menu"); + for (MenuRow row : mMenuRows) { + row.onStreamInfoChanged(); + } + } + + @VisibleForTesting + void disableAnimationForTest() { + if (!Utils.isRunningInTest()) { + throw new RuntimeException("Animation may only be enabled/disabled during tests."); + } + mAnimationDisabledForTest = true; + } + + /** + * A listener which receives the notification when the menu is visible/invisible. + */ + public static abstract class OnMenuVisibilityChangeListener { + /** + * Called when the menu becomes visible/invisible. + */ + public abstract void onMenuVisibilityChange(boolean visible); + } + + private static class MenuWeakHandler extends WeakHandler

{ + public MenuWeakHandler(Menu menu, Looper mainLooper) { + super(mainLooper, menu); + } + + @Override + public void handleMessage(Message msg, @NonNull Menu menu) { + if (msg.what == MSG_HIDE_MENU) { + menu.hide(true); + } + } + } +} diff --git a/src/com/android/tv/menu/MenuLayoutManager.java b/src/com/android/tv/menu/MenuLayoutManager.java new file mode 100644 index 00000000..187d0e14 --- /dev/null +++ b/src/com/android/tv/menu/MenuLayoutManager.java @@ -0,0 +1,819 @@ +/* + * 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.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.support.annotation.UiThread; +import android.support.v4.view.animation.FastOutLinearInInterpolator; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.support.v4.view.animation.LinearOutSlowInInterpolator; +import android.util.Log; +import android.util.Property; +import android.view.View; +import android.view.ViewGroup.MarginLayoutParams; +import android.widget.TextView; + +import com.android.tv.R; +import com.android.tv.util.Utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; + +/** + * A view that represents TV main menu. + */ +@UiThread +public class MenuLayoutManager { + static final String TAG = "MenuLayoutManager"; + static final boolean DEBUG = false; + + // The visible duration of the title before it is hidden. + private static final long TITLE_SHOW_DURATION_BEFORE_HIDDEN_MS = TimeUnit.SECONDS.toMillis(2); + + private final MenuView mMenuView; + private final List mMenuRows = new ArrayList<>(); + private final List mMenuRowViews = new ArrayList<>(); + private final List mRemovingRowViews = new ArrayList<>(); + private int mSelectedPosition = -1; + + private final int mRowAlignFromBottom; + private final int mRowContentsPaddingTop; + private final int mRowContentsPaddingBottomMax; + private final int mRowTitleTextDescenderHeight; + private final int mMenuMarginBottomMin; + private final int mRowTitleHeight; + private final int mRowScrollUpAnimationOffset; + + private final long mRowAnimationDuration; + private final long mOldContentsFadeOutDuration; + private final long mCurrentContentsFadeInDuration; + private final TimeInterpolator mFastOutSlowIn = new FastOutSlowInInterpolator(); + private final TimeInterpolator mFastOutLinearIn = new FastOutLinearInInterpolator(); + private final TimeInterpolator mLinearOutSlowIn = new LinearOutSlowInInterpolator(); + private AnimatorSet mAnimatorSet; + private ObjectAnimator mTitleFadeOutAnimator; + private final List mPropertyValuesAfterAnimation = new ArrayList<>(); + + private TextView mTempTitleViewForOld; + private TextView mTempTitleViewForCurrent; + + public MenuLayoutManager(Context context, MenuView menuView) { + mMenuView = menuView; + // Load dimensions + Resources res = context.getResources(); + mRowAlignFromBottom = res.getDimensionPixelOffset(R.dimen.menu_row_align_from_bottom); + mRowContentsPaddingTop = res.getDimensionPixelOffset(R.dimen.menu_row_contents_padding_top); + mRowContentsPaddingBottomMax = res.getDimensionPixelOffset( + R.dimen.menu_row_contents_padding_bottom_max); + mRowTitleTextDescenderHeight = res.getDimensionPixelOffset( + R.dimen.menu_row_title_text_descender_height); + mMenuMarginBottomMin = res.getDimensionPixelOffset(R.dimen.menu_margin_bottom_min); + mRowTitleHeight = res.getDimensionPixelSize(R.dimen.menu_row_title_height); + mRowScrollUpAnimationOffset = + res.getDimensionPixelOffset(R.dimen.menu_row_scroll_up_anim_offset); + mRowAnimationDuration = res.getInteger(R.integer.menu_row_selection_anim_duration); + mOldContentsFadeOutDuration = res.getInteger( + R.integer.menu_previous_contents_fade_out_duration); + mCurrentContentsFadeInDuration = res.getInteger( + R.integer.menu_current_contents_fade_in_duration); + } + + /** + * Sets the menu rows and views. + */ + public void setMenuRowsAndViews(List menuRows, List menuRowViews) { + mMenuRows.clear(); + mMenuRows.addAll(menuRows); + mMenuRowViews.clear(); + mMenuRowViews.addAll(menuRowViews); + } + + /** + * Layouts main menu view. + * + *

Do not call this method directly. It's supposed to be called only by View.onLayout(). + */ + public void layout(int left, int top, int right, int bottom) { + if (mAnimatorSet != null) { + // Layout will be done after the animation ends. + return; + } + + int count = mMenuRowViews.size(); + MenuRowView currentView = mMenuRowViews.get(mSelectedPosition); + if (currentView.getVisibility() == View.GONE) { + // If the selected row is not visible, select the first visible row. + int firstVisiblePosition = findNextVisiblePosition(-1); + if (firstVisiblePosition != -1) { + mSelectedPosition = firstVisiblePosition; + } else { + // No rows are visible. + return; + } + } + List layouts = getViewLayouts(left, top, right, bottom); + for (int i = 0; i < count; ++i) { + Rect rect = layouts.get(i); + if (rect != null) { + currentView = mMenuRowViews.get(i); + currentView.layout(rect.left, rect.top, rect.right, rect.bottom); + if (DEBUG) dumpChildren("layout()"); + } + } + + // If the contents view is INVISIBLE initially, it should be changed to GONE after layout. + // See MenuRowView.onFinishInflate() for more information + // TODO: Find a better way to resolve this issue.. + for (MenuRowView view : mMenuRowViews) { + if (view.getVisibility() == View.VISIBLE + && view.getContentsView().getVisibility() == View.INVISIBLE) { + view.onDeselected(); + } + } + } + + private int findNextVisiblePosition(int start) { + int count = mMenuRowViews.size(); + for (int i = start + 1; i < count; ++i) { + if (mMenuRowViews.get(i).getVisibility() != View.GONE) { + return i; + } + } + return -1; + } + + private void dumpChildren(String prefix) { + int position = 0; + for (MenuRowView view : mMenuRowViews) { + View title = view.getChildAt(0); + View contents = view.getChildAt(1); + Log.d(TAG, prefix + " position=" + position++ + + " rowView={visiblility=" + view.getVisibility() + + ", alpha=" + view.getAlpha() + + ", translationY=" + view.getTranslationY() + + ", left=" + view.getLeft() + ", top=" + view.getTop() + + ", right=" + view.getRight() + ", bottom=" + view.getBottom() + + "}, title={visiblility=" + title.getVisibility() + + ", alpha=" + title.getAlpha() + + ", translationY=" + title.getTranslationY() + + ", left=" + title.getLeft() + ", top=" + title.getTop() + + ", right=" + title.getRight() + ", bottom=" + title.getBottom() + + "}, contents={visiblility=" + contents.getVisibility() + + ", alpha=" + contents.getAlpha() + + ", translationY=" + contents.getTranslationY() + + ", left=" + contents.getLeft() + ", top=" + contents.getTop() + + ", right=" + contents.getRight() + ", bottom=" + contents.getBottom()+ "}"); + } + } + + /** + * Checks if the view will take up space for the layout not. + * + * @param position The index of the menu row view in the list. This is not the index of the view + * in the screen. + * @param view The menu row view. + * @param rowsToAdd The menu row views to be added in the next layout process. + * @param rowsToRemove The menu row views to be removed in the next layout process. + * @return {@code true} if the view will take up space for the layout, otherwise {@code false}. + */ + private boolean isVisibleInLayout(int position, MenuRowView view, List rowsToAdd, + List rowsToRemove) { + // Checks if the view will be visible or not. + return (view.getVisibility() != View.GONE && !rowsToRemove.contains(position)) + || rowsToAdd.contains(position); + } + + /** + * Calculates and returns a list of the layout bounds of the menu row views for the layout. + * + * @param left The left coordinate of the menu view. + * @param top The top coordinate of the menu view. + * @param right The right coordinate of the menu view. + * @param bottom The bottom coordinate of the menu view. + */ + private List getViewLayouts(int left, int top, int right, int bottom) { + return getViewLayouts(left, top, right, bottom, Collections.emptyList(), + Collections.emptyList()); + } + + /** + * Calculates and returns a list of the layout bounds of the menu row views for the layout. The + * order of the bounds is the same as that of the menu row views. e.g. the second rectangle in + * the list is for the second menu row view in the view list (not the second view in the + * screen). + * + *

It predicts the layout bounds for the next layout process. Some views will be added or + * removed in the layout, so they need to be considered here. + * + * @param left The left coordinate of the menu view. + * @param top The top coordinate of the menu view. + * @param right The right coordinate of the menu view. + * @param bottom The bottom coordinate of the menu view. + * @param rowsToAdd The menu row views to be added in the next layout process. + * @param rowsToRemove The menu row views to be removed in the next layout process. + * @return the layout bounds of the menu row views. + */ + private List getViewLayouts(int left, int top, int right, int bottom, + List rowsToAdd, List rowsToRemove) { + // The coordinates should be relative to the parent. + int relativeLeft = 0; + int relateiveRight = right - left; + int relativeBottom = bottom - top; + + List layouts = new ArrayList<>(); + int count = mMenuRowViews.size(); + MenuRowView selectedView = mMenuRowViews.get(mSelectedPosition); + int rowTitleHeight = selectedView.getTitleView().getMeasuredHeight(); + int rowContentsHeight = selectedView.getPreferredContentsHeight(); + // Calculate for the selected row first. + // The distance between the bottom of the screen and the vertical center of the contents + // should be kept fixed. For more information, please see the redlines. + int childTop = relativeBottom - mRowAlignFromBottom - rowContentsHeight / 2 + - mRowContentsPaddingTop - rowTitleHeight; + int childBottom = relativeBottom; + int position = mSelectedPosition + 1; + for (; position < count; ++position) { + // Find and layout the next row to calculate the bottom line of the selected row. + MenuRowView nextView = mMenuRowViews.get(position); + if (isVisibleInLayout(position, nextView, rowsToAdd, rowsToRemove)) { + int nextTitleTopMax = relativeBottom - mMenuMarginBottomMin - rowTitleHeight + + mRowTitleTextDescenderHeight; + int childBottomMax = relativeBottom - mRowAlignFromBottom + rowContentsHeight / 2 + + mRowContentsPaddingBottomMax - rowTitleHeight; + childBottom = Math.min(nextTitleTopMax, childBottomMax); + layouts.add(new Rect(relativeLeft, childBottom, relateiveRight, relativeBottom)); + break; + } else { + // null means that the row is GONE. + layouts.add(null); + } + } + layouts.add(0, new Rect(relativeLeft, childTop, relateiveRight, childBottom)); + // Layout the previous rows. + for (int i = mSelectedPosition - 1; i >= 0; --i) { + MenuRowView view = mMenuRowViews.get(i); + if (isVisibleInLayout(i, view, rowsToAdd, rowsToRemove)) { + childTop -= mRowTitleHeight; + childBottom = childTop + rowTitleHeight; + layouts.add(0, new Rect(relativeLeft, childTop, relateiveRight, childBottom)); + } else { + layouts.add(0, null); + } + } + // Move all the next rows to the below of the screen. + childTop = relativeBottom; + for (++position; position < count; ++position) { + MenuRowView view = mMenuRowViews.get(position); + if (isVisibleInLayout(position, view, rowsToAdd, rowsToRemove)) { + childBottom = childTop + rowTitleHeight; + layouts.add(new Rect(relativeLeft, childTop, relateiveRight, childBottom)); + childTop += mRowTitleHeight; + } else { + layouts.add(null); + } + } + return layouts; + } + + /** + * Move the current selection to the given {@code position}. + */ + public void setSelectedPosition(int position) { + if (DEBUG) { + Log.d(TAG, "setSelectedPosition(position=" + position + ") {previousPosition=" + + mSelectedPosition + "}"); + } + if (mSelectedPosition == position) { + return; + } + if (position < 0 || position >= mMenuRowViews.size()) { + String msg = "Invalid position: " + position; + Utils.engThrowElseWarn(TAG, msg, new IllegalArgumentException(msg)); + return; + } + if (mSelectedPosition >= 0 && mSelectedPosition < mMenuRowViews.size()) { + mMenuRowViews.get(mSelectedPosition).onDeselected(); + } + mSelectedPosition = position; + if (mSelectedPosition >= 0 && mSelectedPosition < mMenuRowViews.size()) { + mMenuRowViews.get(mSelectedPosition).onSelected(false); + } + if (mMenuView.getVisibility() == View.VISIBLE) { + // Request focus after the new contents view shows up. + mMenuView.requestFocus(); + // Adjust the position of the selected row. + mMenuView.requestLayout(); + } + } + + /** + * Move the current selection to the given {@code position} with animation. + * The animation specification is included in http://b/21069476 + */ + public void setSelectedPositionSmooth(final int position) { + if (DEBUG) { + Log.d(TAG, "setSelectedPositionSmooth(position=" + position + ") {previousPosition=" + + mSelectedPosition + "}"); + } + if (mMenuView.getVisibility() != View.VISIBLE) { + setSelectedPosition(position); + return; + } + if (mSelectedPosition == position) { + return; + } + if (mSelectedPosition < 0 || mSelectedPosition >= mMenuRowViews.size()) { + String msg = "No previous selection: " + mSelectedPosition; + Utils.engThrowElseWarn(TAG, msg, new IllegalStateException(msg)); + return; + } + if (position < 0 || position >= mMenuRowViews.size()) { + String msg = "Invalid position: " + position; + Utils.engThrowElseWarn(TAG, msg, new IllegalArgumentException(msg)); + return; + } + if (mAnimatorSet != null) { + // Do not cancel the animation here. The property values should be set to the end values + // when the animation finishes. + mAnimatorSet.end(); + } + if (mTitleFadeOutAnimator != null) { + // Cancel the animation instead of ending it in order that the title animation starts + // again from the intermediate state. + mTitleFadeOutAnimator.cancel(); + } + final int oldPosition = mSelectedPosition; + mSelectedPosition = position; + if (DEBUG) dumpChildren("startRowAnimation()"); + + MenuRowView currentView = mMenuRowViews.get(position); + // Show the children of the next row. + currentView.getTitleView().setVisibility(View.VISIBLE); + currentView.getContentsView().setVisibility(View.VISIBLE); + // Request focus after the new contents view shows up. + mMenuView.requestFocus(); + if (mTempTitleViewForOld == null) { + // Initialize here because we don't know when the views are inflated. + mTempTitleViewForOld = + (TextView) mMenuView.findViewById(R.id.temp_title_for_old); + mTempTitleViewForCurrent = + (TextView) mMenuView.findViewById(R.id.temp_title_for_current); + } + + // Animations. + mPropertyValuesAfterAnimation.clear(); + List animators = new ArrayList<>(); + boolean scrollDown = position > oldPosition; + List layouts = getViewLayouts(mMenuView.getLeft(), mMenuView.getTop(), + mMenuView.getRight(), mMenuView.getBottom()); + + // Old row. + MenuRow oldRow = mMenuRows.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) + .setDuration(mOldContentsFadeOutDuration)); + final TextView oldTitleView = oldView.getTitleView(); + setTempTitleView(mTempTitleViewForOld, oldTitleView); + Rect oldLayoutRect = layouts.get(oldPosition); + if (scrollDown) { + // Old title view. + if (oldRow.hideTitleWhenSelected() && oldTitleView.getVisibility() != View.VISIBLE) { + // This case is not included in the animation specification. + mTempTitleViewForOld.setScaleX(1.0f); + mTempTitleViewForOld.setScaleY(1.0f); + animators.add(createAlphaAnimator(mTempTitleViewForOld, 0.0f, + oldView.getTitleViewAlphaDeselected(), mFastOutLinearIn)); + int offset = oldLayoutRect.top - mTempTitleViewForOld.getTop(); + animators.add(createTranslationYAnimator(mTempTitleViewForOld, + offset + mRowScrollUpAnimationOffset, offset)); + } else { + animators.add(createScaleXAnimator(mTempTitleViewForOld, + oldView.getTitleViewScaleSelected(), 1.0f)); + animators.add(createScaleYAnimator(mTempTitleViewForOld, + oldView.getTitleViewScaleSelected(), 1.0f)); + animators.add(createAlphaAnimator(mTempTitleViewForOld, oldTitleView.getAlpha(), + oldView.getTitleViewAlphaDeselected(), mLinearOutSlowIn)); + animators.add(createTranslationYAnimator(mTempTitleViewForOld, 0, + oldLayoutRect.top - mTempTitleViewForOld.getTop())); + } + oldTitleView.setAlpha(oldView.getTitleViewAlphaDeselected()); + oldTitleView.setVisibility(View.INVISIBLE); + } else { + Rect currentLayoutRect = new Rect(layouts.get(position)); + // Old title view. + // The move distance in the specification is 32dp(mRowScrollUpAnimationOffset). + // But if the height of the upper row is small, the upper row will move down a lot. In + // this case, this row needs to move more than the specification to avoid the overlap of + // the two titles. + // The maximum is to the top of the start position of mTempTitleViewForOld. + int distanceCurrentTitle = currentLayoutRect.top - currentView.getTop(); + int distance = Math.max(mRowScrollUpAnimationOffset, distanceCurrentTitle); + int distanceToTopOfSecondTitle = oldLayoutRect.top - mRowScrollUpAnimationOffset + - oldView.getTop(); + animators.add(createTranslationYAnimator(oldTitleView, 0.0f, + Math.min(distance, distanceToTopOfSecondTitle))); + animators.add(createAlphaAnimator(oldTitleView, 1.0f, 0.0f, 1.0f, mLinearOutSlowIn) + .setDuration(mOldContentsFadeOutDuration)); + animators.add(createScaleXAnimator(oldTitleView, + oldView.getTitleViewScaleSelected(), 1.0f)); + animators.add(createScaleYAnimator(oldTitleView, + oldView.getTitleViewScaleSelected(), 1.0f)); + mTempTitleViewForOld.setScaleX(1.0f); + mTempTitleViewForOld.setScaleY(1.0f); + animators.add(createAlphaAnimator(mTempTitleViewForOld, 0.0f, + oldView.getTitleViewAlphaDeselected(), mFastOutLinearIn)); + int offset = oldLayoutRect.top - mTempTitleViewForOld.getTop(); + animators.add(createTranslationYAnimator(mTempTitleViewForOld, + offset - mRowScrollUpAnimationOffset, offset)); + } + // 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. + setTempTitleView(mTempTitleViewForCurrent, currentTitleView); + // The move distance in the specification is 32dp(mRowScrollUpAnimationOffset). + // But if the height of the upper row is small, the upper row will move up a lot. In + // this case, this row needs to start the move from more than the specification to avoid + // the overlap of the two titles. + // The maximum is to the top of the end position of mTempTitleViewForCurrent. + int distanceOldTitle = oldView.getTop() - oldLayoutRect.top; + int distance = Math.max(mRowScrollUpAnimationOffset, distanceOldTitle); + int distanceTopOfSecondTitle = currentView.getTop() - mRowScrollUpAnimationOffset + - currentLayoutRect.top; + animators.add(createTranslationYAnimator(currentTitleView, + Math.min(distance, distanceTopOfSecondTitle), 0.0f)); + currentView.setTop(currentLayoutRect.top); + ObjectAnimator animator = createAlphaAnimator(currentTitleView, 0.0f, 1.0f, + mFastOutLinearIn).setDuration(mCurrentContentsFadeInDuration); + animator.setStartDelay(mOldContentsFadeOutDuration); + currentTitleView.setAlpha(0.0f); + animators.add(animator); + animators.add(createScaleXAnimator(currentTitleView, 1.0f, + currentView.getTitleViewScaleSelected())); + animators.add(createScaleYAnimator(currentTitleView, 1.0f, + currentView.getTitleViewScaleSelected())); + animators.add(createTranslationYAnimator(mTempTitleViewForCurrent, 0.0f, + -mRowScrollUpAnimationOffset)); + animators.add(createAlphaAnimator(mTempTitleViewForCurrent, + currentView.getTitleViewAlphaDeselected(), 0, mLinearOutSlowIn)); + // Current contents view. + animators.add(createTranslationYAnimator(currentContentsView, + mRowScrollUpAnimationOffset, 0.0f)); + animator = createAlphaAnimator(currentContentsView, 0.0f, 1.0f, mFastOutLinearIn) + .setDuration(mCurrentContentsFadeInDuration); + animator.setStartDelay(mOldContentsFadeOutDuration); + animators.add(animator); + } else { + currentView.setBottom(currentLayoutRect.bottom); + // Current title view. + int currentViewOffset = currentLayoutRect.top - currentView.getTop(); + animators.add(createTranslationYAnimator(currentTitleView, 0, currentViewOffset)); + animators.add(createAlphaAnimator(currentTitleView, + currentView.getTitleViewAlphaDeselected(), 1.0f, mFastOutSlowIn)); + animators.add(createScaleXAnimator(currentTitleView, 1.0f, + currentView.getTitleViewScaleSelected())); + animators.add(createScaleYAnimator(currentTitleView, 1.0f, + currentView.getTitleViewScaleSelected())); + // Current contents view. + animators.add(createTranslationYAnimator(currentContentsView, + currentViewOffset - mRowScrollUpAnimationOffset, currentViewOffset)); + ObjectAnimator animator = createAlphaAnimator(currentContentsView, 0.0f, 1.0f, + mFastOutLinearIn).setDuration(mCurrentContentsFadeInDuration); + animator.setStartDelay(mOldContentsFadeOutDuration); + animators.add(animator); + } + // Next row. + int nextPosition; + if (scrollDown) { + nextPosition = findNextVisiblePosition(position); + if (nextPosition != -1) { + MenuRowView nextView = mMenuRowViews.get(nextPosition); + Rect nextLayoutRect = layouts.get(nextPosition); + animators.add(createTranslationYAnimator(nextView, + nextLayoutRect.top + mRowScrollUpAnimationOffset - nextView.getTop(), + nextLayoutRect.top - nextView.getTop())); + animators.add(createAlphaAnimator(nextView, 0.0f, 1.0f, mFastOutLinearIn)); + } + } else { + nextPosition = findNextVisiblePosition(oldPosition); + if (nextPosition != -1) { + MenuRowView nextView = mMenuRowViews.get(nextPosition); + animators.add(createTranslationYAnimator(nextView, 0, mRowScrollUpAnimationOffset)); + animators.add(createAlphaAnimator(nextView, + nextView.getTitleViewAlphaDeselected(), 0.0f, 1.0f, mLinearOutSlowIn)); + } + } + // Other rows. + int count = mMenuRowViews.size(); + for (int i = 0; i < count; ++i) { + MenuRowView view = mMenuRowViews.get(i); + if (view.getVisibility() == View.VISIBLE && i != oldPosition && i != position + && i != nextPosition) { + Rect rect = layouts.get(i); + animators.add(createTranslationYAnimator(view, 0, rect.top - view.getTop())); + } + } + // Run animation. + final List propertyValuesAfterAnimation = new ArrayList<>(); + propertyValuesAfterAnimation.addAll(mPropertyValuesAfterAnimation); + mAnimatorSet = new AnimatorSet(); + mAnimatorSet.playTogether(animators); + mAnimatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + if (DEBUG) dumpChildren("onRowAnimationEndBefore"); + mAnimatorSet = null; + // The property values which are different from the end values and need to be + // changed after the animation are set here. + // e.g. setting translationY to 0, alpha of the contents view to 1. + for (ViewPropertyValueHolder holder : propertyValuesAfterAnimation) { + holder.property.set(holder.view, holder.value); + } + 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(), + mMenuView.getBottom()); + if (DEBUG) dumpChildren("onRowAnimationEndAfter"); + + MenuRow currentRow = mMenuRows.get(position); + if (currentRow.hideTitleWhenSelected()) { + View titleView = mMenuRowViews.get(position).getTitleView(); + mTitleFadeOutAnimator = createAlphaAnimator(titleView, titleView.getAlpha(), + 0.0f, mLinearOutSlowIn); + mTitleFadeOutAnimator.setStartDelay(TITLE_SHOW_DURATION_BEFORE_HIDDEN_MS); + mTitleFadeOutAnimator.addListener(new AnimatorListenerAdapter() { + private boolean mCanceled; + + @Override + public void onAnimationCancel(Animator animator) { + mCanceled = true; + } + + @Override + public void onAnimationEnd(Animator animator) { + mTitleFadeOutAnimator = null; + if (!mCanceled) { + mMenuRowViews.get(position).onSelected(false); + } + } + }); + mTitleFadeOutAnimator.start(); + } + } + }); + mAnimatorSet.start(); + if (DEBUG) dumpChildren("startedRowAnimation()"); + } + + private void setTempTitleView(TextView dest, TextView src) { + dest.setVisibility(View.VISIBLE); + dest.setText(src.getText()); + dest.setTranslationY(0.0f); + if (src.getVisibility() == View.VISIBLE) { + dest.setAlpha(src.getAlpha()); + dest.setScaleX(src.getScaleX()); + dest.setScaleY(src.getScaleY()); + } else { + dest.setAlpha(0.0f); + dest.setScaleX(1.0f); + dest.setScaleY(1.0f); + } + View parent = (View) src.getParent(); + dest.setLeft(src.getLeft() + parent.getLeft()); + dest.setRight(src.getRight() + parent.getLeft()); + dest.setTop(src.getTop() + parent.getTop()); + dest.setBottom(src.getBottom() + parent.getTop()); + } + + /** + * Called when the menu row information is updated. The add/remove animation of the row views + * will be started. + * + *

Note that the current row should not be removed. + */ + public void onMenuRowUpdated() { + if (mMenuView.getVisibility() != View.VISIBLE) { + int count = mMenuRowViews.size(); + for (int i = 0; i < count; ++i) { + mMenuRowViews.get(i).setVisibility(mMenuRows.get(i).isVisible() ? View.VISIBLE + : View.GONE); + } + return; + } + + List addedRowViews = new ArrayList<>(); + List removedRowViews = new ArrayList<>(); + Map offsetsToMove = new HashMap<>(); + int added = 0; + for (int i = mSelectedPosition - 1; i >= 0; --i) { + MenuRow row = mMenuRows.get(i); + MenuRowView view = mMenuRowViews.get(i); + if (row.isVisible() && (view.getVisibility() == View.GONE + || mRemovingRowViews.contains(i))) { + // Removing rows are still VISIBLE. + addedRowViews.add(i); + ++added; + } else if (!row.isVisible() && view.getVisibility() == View.VISIBLE) { + removedRowViews.add(i); + --added; + } else if (added != 0) { + offsetsToMove.put(i, -added); + } + } + added = 0; + int count = mMenuRowViews.size(); + for (int i = mSelectedPosition + 1; i < count; ++i) { + MenuRow row = mMenuRows.get(i); + MenuRowView view = mMenuRowViews.get(i); + if (row.isVisible() && (view.getVisibility() == View.GONE + || mRemovingRowViews.contains(i))) { + // Removing rows are still VISIBLE. + addedRowViews.add(i); + ++added; + } else if (!row.isVisible() && view.getVisibility() == View.VISIBLE) { + removedRowViews.add(i); + --added; + } else if (added != 0) { + offsetsToMove.put(i, added); + } + } + if (addedRowViews.size() == 0 && removedRowViews.size() == 0) { + return; + } + + if (mAnimatorSet != null) { + // Do not cancel the animation here. The property values should be set to the end values + // when the animation finishes. + mAnimatorSet.end(); + } + if (mTitleFadeOutAnimator != null) { + mTitleFadeOutAnimator.end(); + } + mPropertyValuesAfterAnimation.clear(); + List animators = new ArrayList<>(); + List layouts = getViewLayouts(mMenuView.getLeft(), mMenuView.getTop(), + mMenuView.getRight(), mMenuView.getBottom(), addedRowViews, removedRowViews); + for (int position : addedRowViews) { + MenuRowView view = mMenuRowViews.get(position); + view.setVisibility(View.VISIBLE); + Rect rect = layouts.get(position); + // TODO: The animation is not visible when it is shown for the first time. Need to find + // a better way to resolve this issue. + view.layout(rect.left, rect.top, rect.right, rect.bottom); + View titleView = view.getTitleView(); + MarginLayoutParams params = (MarginLayoutParams) titleView.getLayoutParams(); + titleView.layout(view.getPaddingLeft() + params.leftMargin, + view.getPaddingTop() + params.topMargin, + rect.right - rect.left - view.getPaddingRight() - params.rightMargin, + rect.bottom - rect.top - view.getPaddingBottom() - params.bottomMargin); + animators.add(createAlphaAnimator(view, 0.0f, 1.0f, mFastOutLinearIn)); + } + for (int position : removedRowViews) { + MenuRowView view = mMenuRowViews.get(position); + animators.add(createAlphaAnimator(view, 1.0f, 0.0f, 1.0f, mLinearOutSlowIn)); + } + for (Entry entry : offsetsToMove.entrySet()) { + MenuRowView view = mMenuRowViews.get(entry.getKey()); + animators.add(createTranslationYAnimator(view, 0, entry.getValue() * mRowTitleHeight)); + } + // Run animation. + final List propertyValuesAfterAnimation = new ArrayList<>(); + propertyValuesAfterAnimation.addAll(mPropertyValuesAfterAnimation); + mRemovingRowViews.clear(); + mRemovingRowViews.addAll(removedRowViews); + mAnimatorSet = new AnimatorSet(); + mAnimatorSet.playTogether(animators); + mAnimatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mAnimatorSet = null; + // The property values which are different from the end values and need to be + // changed after the animation are set here. + // e.g. setting translationY to 0, alpha of the contents view to 1. + for (ViewPropertyValueHolder holder : propertyValuesAfterAnimation) { + holder.property.set(holder.view, holder.value); + } + for (int position : mRemovingRowViews) { + mMenuRowViews.get(position).setVisibility(View.GONE); + } + layout(mMenuView.getLeft(), mMenuView.getTop(), mMenuView.getRight(), + mMenuView.getBottom()); + } + }); + mAnimatorSet.start(); + if (DEBUG) dumpChildren("onMenuRowUpdated()"); + } + + private ObjectAnimator createTranslationYAnimator(View view, float from, float to) { + ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, from, to); + animator.setDuration(mRowAnimationDuration); + animator.setInterpolator(mFastOutSlowIn); + mPropertyValuesAfterAnimation.add(new ViewPropertyValueHolder(View.TRANSLATION_Y, view, 0)); + return animator; + } + + private ObjectAnimator createAlphaAnimator(View view, float from, float to, + TimeInterpolator interpolator) { + ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.ALPHA, from, to); + animator.setDuration(mRowAnimationDuration); + animator.setInterpolator(interpolator); + return animator; + } + + private ObjectAnimator createAlphaAnimator(View view, float from, float to, float end, + TimeInterpolator interpolator) { + ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.ALPHA, from, to); + animator.setDuration(mRowAnimationDuration); + animator.setInterpolator(interpolator); + mPropertyValuesAfterAnimation.add(new ViewPropertyValueHolder(View.ALPHA, view, end)); + return animator; + } + + private ObjectAnimator createScaleXAnimator(View view, float from, float to) { + ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.SCALE_X, from, to); + animator.setDuration(mRowAnimationDuration); + animator.setInterpolator(mFastOutSlowIn); + return animator; + } + + private ObjectAnimator createScaleYAnimator(View view, float from, float to) { + ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.SCALE_Y, from, to); + animator.setDuration(mRowAnimationDuration); + animator.setInterpolator(mFastOutSlowIn); + return animator; + } + + /** + * Returns the current position. + */ + public int getSelectedPosition() { + return mSelectedPosition; + } + + private static final class ViewPropertyValueHolder { + public Property property; + public View view; + public float value; + + public ViewPropertyValueHolder(Property property, View view, float value) { + this.property = property; + this.view = view; + this.value = value; + } + } + + /** + * Called when the menu becomes visible. + */ + public void onMenuShow() { + } + + /** + * Called when the menu becomes hidden. + */ + public void onMenuHide() { + if (mAnimatorSet != null) { + mAnimatorSet.end(); + mAnimatorSet = null; + } + // Should be finished after the animator set. + if (mTitleFadeOutAnimator != null) { + mTitleFadeOutAnimator.end(); + mTitleFadeOutAnimator = null; + } + } +} diff --git a/src/com/android/tv/menu/MenuRow.java b/src/com/android/tv/menu/MenuRow.java index 38cda0bf..fe73edd2 100644 --- a/src/com/android/tv/menu/MenuRow.java +++ b/src/com/android/tv/menu/MenuRow.java @@ -18,8 +18,6 @@ package com.android.tv.menu; import android.content.Context; -import com.android.tv.MainActivity; - /** * A base class of the item which will be displayed in the main menu. * It contains the data such as title to represent a row. @@ -30,15 +28,17 @@ public abstract class MenuRow { private final Context mContext; private final String mTitle; private final int mHeight; + private final Menu mMenu; // TODO: Check if the heightResId is really necessary. - public MenuRow(Context context, int titleResId, int heightResId) { - this(context, context.getString(titleResId), heightResId); + public MenuRow(Context context, Menu menu, int titleResId, int heightResId) { + this(context, menu, context.getString(titleResId), heightResId); } - public MenuRow(Context context, String title, int heightResId) { + public MenuRow(Context context, Menu menu, String title, int heightResId) { mContext = context; mTitle = title; + mMenu = menu; mHeight = context.getResources().getDimensionPixelSize(heightResId); } @@ -49,8 +49,11 @@ public abstract class MenuRow { return mContext; } - protected MainActivity getMainActivity() { - return (MainActivity) mContext; + /** + * Returns the menu object. + */ + public Menu getMenu() { + return mMenu; } /** @@ -96,4 +99,21 @@ public abstract class MenuRow { * Returns the ID of this row. This ID is used to select the row in the main menu. */ abstract public String getId(); + + /** + * This method is called when recent channels are changed. + */ + public void onRecentChannelsChanged() { } + + /** + * This method is called when stream information is changed. + */ + public void onStreamInfoChanged() { } + + /** + * Returns whether to hide the title when the row is selected. + */ + public boolean hideTitleWhenSelected() { + return false; + } } diff --git a/src/com/android/tv/menu/MenuRowFactory.java b/src/com/android/tv/menu/MenuRowFactory.java new file mode 100644 index 00000000..b0b000f1 --- /dev/null +++ b/src/com/android/tv/menu/MenuRowFactory.java @@ -0,0 +1,118 @@ +/* + * 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.support.annotation.Nullable; +import android.text.TextUtils; + +import com.android.tv.MainActivity; +import com.android.tv.R; +import com.android.tv.customization.CustomAction; +import com.android.tv.customization.TvCustomizationManager; + +import java.util.List; + +/** + * A factory class to create menu rows. + */ +public class MenuRowFactory { + private final MainActivity mMainActivity; + private final TvCustomizationManager mTvCustomizationManager; + + /** + * A constructor. + */ + public MenuRowFactory(MainActivity mainActivity) { + mMainActivity = mainActivity; + mTvCustomizationManager = new TvCustomizationManager(mainActivity); + mTvCustomizationManager.initialize(); + } + + /** + * Creates an object corresponding to the given {@code key}. + */ + @Nullable + public MenuRow createMenuRow(Menu menu, Class key) { + if (PlayControlsRow.class.equals(key)) { + return new PlayControlsRow(mMainActivity, menu, mMainActivity.getTimeShiftManager()); + } else if (ChannelsRow.class.equals(key)) { + return new ChannelsRow(mMainActivity, menu, mMainActivity.getProgramDataManager()); + } else if (PartnerRow.class.equals(key)) { + List customActions = mTvCustomizationManager.getCustomActions( + TvCustomizationManager.ID_PARTNER_ROW); + String title = mTvCustomizationManager.getPartnerRowTitle(); + if (customActions != null && !TextUtils.isEmpty(title)) { + return new PartnerRow(mMainActivity, menu, title, customActions); + } + return null; + } 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; + } + + /** + * A menu row which represents the TV options row. + */ + public static class TvOptionsRow extends ItemListRow { + private TvOptionsRow(Context context, Menu menu, List customActions) { + super(context, menu, R.string.menu_title_options, R.dimen.action_card_height, + new TvOptionsRowAdapter(context, customActions)); + } + + @Override + public void onStreamInfoChanged() { + if (getMenu().isActive()) { + update(); + } + } + } + + /** + * 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 { + private PartnerRow(Context context, Menu menu, String title, + List customActions) { + super(context, menu, title, R.dimen.action_card_height, + new PartnerOptionsRowAdapter(context, customActions)); + } + } +} diff --git a/src/com/android/tv/menu/MenuRowView.java b/src/com/android/tv/menu/MenuRowView.java index 31ab3d93..a6d8c990 100644 --- a/src/com/android/tv/menu/MenuRowView.java +++ b/src/com/android/tv/menu/MenuRowView.java @@ -20,7 +20,6 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; import android.support.annotation.NonNull; -import android.support.v17.leanback.widget.VerticalGridView; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; @@ -30,10 +29,10 @@ import android.widget.LinearLayout; import android.widget.TextView; import com.android.tv.R; -import com.android.tv.menu.MenuView.MenuShowReason; +import com.android.tv.menu.Menu.MenuShowReason; public abstract class MenuRowView extends LinearLayout { - private static final String TAG = MenuRowView.class.getSimpleName(); + private static final String TAG = "MenuRowView"; private static final boolean DEBUG = false; /** @@ -57,13 +56,9 @@ public abstract class MenuRowView extends LinearLayout { private TextView mTitleView; private View mContentsView; - private MenuView mMenuView; - private VerticalGridView mParentView; - private boolean mIsSelected; - private final float mTitleScaleSelected; - private final float mTitleAlphaSelected; - private final float mTitleAlphaDeselected; + private final float mTitleViewAlphaDeselected; + private final float mTitleViewScaleSelected; /** * The lastly focused view. It is used to keep the focus while navigating the menu rows and @@ -79,6 +74,20 @@ public abstract class MenuRowView extends LinearLayout { } }; + /** + * Returns the alpha value of the title view when it's deselected. + */ + public float getTitleViewAlphaDeselected() { + return mTitleViewAlphaDeselected; + } + + /** + * Returns the scale value of the title view when it's selected. + */ + public float getTitleViewScaleSelected() { + return mTitleViewScaleSelected; + } + public MenuRowView(Context context) { this(context, null); } @@ -93,26 +102,15 @@ public abstract class MenuRowView extends LinearLayout { public MenuRowView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - - mTitleScaleSelected = getTitleScaleSelected(); - mTitleAlphaSelected = getTitleAlphaSelected(); + Resources res = context.getResources(); TypedValue outValue = new TypedValue(); - context.getResources().getValue( - R.dimen.menu_row_title_alpha_deselected, outValue, true); - mTitleAlphaDeselected = outValue.getFloat(); - } - - protected float getTitleScaleSelected() { - Resources res = getContext().getResources(); - int textSizeSelected = + res.getValue(R.dimen.menu_row_title_alpha_deselected, outValue, true); + mTitleViewAlphaDeselected = outValue.getFloat(); + float textSizeSelected = res.getDimensionPixelSize(R.dimen.menu_row_title_text_size_selected); - int textSizeDeselected = + float textSizeDeselected = res.getDimensionPixelSize(R.dimen.menu_row_title_text_size_deselected); - return (float) textSizeSelected / textSizeDeselected; - } - - protected float getTitleAlphaSelected() { - return 1.0f; + mTitleViewScaleSelected = textSizeSelected / textSizeDeselected; } @Override @@ -125,6 +123,11 @@ public abstract class MenuRowView extends LinearLayout { if (mContentsView instanceof ViewGroup) { setOnFocusChangeListenerToChildren((ViewGroup) mContentsView); } + // Make contents view invisible in order that the view participates in the initial layout. + // The visibility is set to GONE after the first layout finishes. + // If not, we can't see the contents view animation for the first time it is shown. + // TODO: Find a better way to resolve this issue. + mContentsView.setVisibility(INVISIBLE); } private void setOnFocusChangeListenerToChildren(ViewGroup parent) { @@ -142,15 +145,18 @@ public abstract class MenuRowView extends LinearLayout { abstract protected int getContentsViewId(); - protected View getContentsView() { - return mContentsView; + /** + * Returns the title view. + */ + public final TextView getTitleView() { + return mTitleView; } - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - updateView(mParentView.getChildAdapterPosition(this) == mParentView.getSelectedPosition() - ? ANIM_NONE_SELECTED : ANIM_NONE_DESELECTED); + /** + * Returns the contents view. + */ + public final View getContentsView() { + return mContentsView; } /** @@ -164,143 +170,19 @@ public abstract class MenuRowView extends LinearLayout { mLastFocusView = null; } - private void updateView(int animationType) { - boolean isSelected = animationType == ANIM_SELECTED || animationType == ANIM_NONE_SELECTED; - if (mIsSelected && isSelected) { - // Prevent from selected again so later calls to {@link updateView} cancels animation. - return; - } - mIsSelected = isSelected; - updateRowView(animationType); - updateTitleView(animationType); - } - - private void updateRowView(int animationType) { - mContentsView.animate().cancel(); - mContentsView.setAlpha(1f); - switch (animationType) { - case ANIM_NONE_SELECTED: { - mContentsView.setVisibility(View.VISIBLE); - break; - } - case ANIM_NONE_DESELECTED: { - mContentsView.setVisibility(View.GONE); - break; - } - case ANIM_SELECTED: { - mContentsView.setVisibility(View.VISIBLE); - mContentsView.setAlpha(0f); - mContentsView.animate() - .alpha(1f) - .setDuration(getMenuView().getRowSelectionAnimationDurationMs()) - .withLayer(); - break; - } - case ANIM_DESELECTED: { - mContentsView.setVisibility(View.GONE); - break; - } - } - } - - private void updateTitleView(int animationType) { - boolean withAnimation = animationType == ANIM_SELECTED || animationType == ANIM_DESELECTED; - int duration = withAnimation ? getMenuView().getRowSelectionAnimationDurationMs() : 0; - - mTitleView.animate().cancel(); - switch (animationType) { - case ANIM_SELECTED: - mTitleView.animate() - .alpha(mTitleAlphaSelected) - .scaleX(mTitleScaleSelected) - .scaleY(mTitleScaleSelected) - .setDuration(duration) - .withLayer(); - break; - case ANIM_NONE_SELECTED: - mTitleView.setAlpha(mTitleAlphaSelected); - mTitleView.setScaleX(mTitleScaleSelected); - mTitleView.setScaleY(mTitleScaleSelected); - break; - case ANIM_DESELECTED: - mTitleView.animate() - .alpha(mTitleAlphaDeselected) - .scaleX(1f) - .scaleY(1f) - .setDuration(duration) - .withLayer(); - break; - case ANIM_NONE_DESELECTED: - mTitleView.setAlpha(mTitleAlphaDeselected); - mTitleView.setScaleX(1f); - mTitleView.setScaleY(1f); - break; - } - } - - /** - * Updates the view contents. - * This method is called when the row is selected. - */ - public void updateView(boolean withAnimation) { - int position = mParentView.getChildAdapterPosition(this); - int selectedPosition = mParentView.getSelectedPosition(); - int animationType = ANIM_NONE_DESELECTED; - if (withAnimation) { - boolean scrollUp = mMenuView.getPreviousSelectedPosition() > selectedPosition; - switch (position - selectedPosition) { - case -2: - animationType = ANIM_NONE_DESELECTED; - break; - case -1: - animationType = scrollUp ? ANIM_NONE_DESELECTED : ANIM_DESELECTED; - break; - case 0: - animationType = ANIM_SELECTED; - break; - case 1: - animationType = scrollUp ? ANIM_DESELECTED : ANIM_NONE_DESELECTED; - break; - case 2: - animationType = ANIM_NONE_DESELECTED; - break; - } - } else { - animationType = (position == selectedPosition) - ? ANIM_NONE_SELECTED : ANIM_NONE_DESELECTED; - } - updateView(animationType); - } - - protected MenuView getMenuView() { - return mMenuView; - } - - public void setMenuView(MenuView view) { - mMenuView = view; - } - - public void setParentView(VerticalGridView view) { - mParentView = view; + protected Menu getMenu() { + return mRow == null ? null : mRow.getMenu(); } public void onBind(MenuRow row) { if (DEBUG) Log.d(TAG, "onBind: row=" + row); mRow = row; mTitleView.setText(row.getTitle()); - - // mListView includes paddings to avoid an artifact while alpha animation. - // See res/layout/item_list.xml for more information. - ViewGroup.LayoutParams lp = mContentsView.getLayoutParams(); - lp.height = row.getHeight() + mMenuView.getItemPaddingHeight() - - getContext().getResources().getDimensionPixelSize( - R.dimen.menu_list_margin_bottom); } @Override protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { // Expand view here so initial focused item can be shown. - updateView(ANIM_SELECTED); return getInitialFocusView().requestFocus(); } @@ -337,4 +219,41 @@ public abstract class MenuRowView extends LinearLayout { public String getRowId() { return mRow == null ? null : mRow.getId(); } + + /** + * Called when this row is selected. + * + * @param showTitle If {@code true}, the title is not hidden immediately after the row is + * selected even though hideTitleWhenSelected() is {@code true}. + */ + public void onSelected(boolean showTitle) { + if (mRow.hideTitleWhenSelected() && !showTitle) { + // Title view should participate in the layout even though it is not visible. + mTitleView.setVisibility(INVISIBLE); + } else { + mTitleView.setVisibility(VISIBLE); + mTitleView.setAlpha(1.0f); + mTitleView.setScaleX(mTitleViewScaleSelected); + mTitleView.setScaleY(mTitleViewScaleSelected); + } + mContentsView.setVisibility(VISIBLE); + } + + /** + * Called when this row is deselected. + */ + public void onDeselected() { + mTitleView.setVisibility(VISIBLE); + mTitleView.setAlpha(mTitleViewAlphaDeselected); + mTitleView.setScaleX(1.0f); + mTitleView.setScaleY(1.0f); + mContentsView.setVisibility(GONE); + } + + /** + * Returns the preferred height of the contents view. The top/bottom padding is excluded. + */ + public int getPreferredContentsHeight() { + return mRow.getHeight(); + } } diff --git a/src/com/android/tv/menu/MenuView.java b/src/com/android/tv/menu/MenuView.java index 92243e13..df91ddf3 100644 --- a/src/com/android/tv/menu/MenuView.java +++ b/src/com/android/tv/menu/MenuView.java @@ -16,129 +16,35 @@ package com.android.tv.menu; -import android.animation.Animator; -import android.animation.AnimatorInflater; -import android.animation.AnimatorListenerAdapter; import android.content.Context; -import android.content.res.Resources; -import android.support.annotation.IntDef; -import android.support.v17.leanback.widget.OnChildSelectedListener; -import android.support.v17.leanback.widget.VerticalGridView; -import android.support.v7.widget.RecyclerView; -import android.text.TextUtils; +import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.ViewTreeObserver.OnGlobalFocusChangeListener; import android.widget.FrameLayout; -import android.widget.OverScroller; -import com.android.tv.ChannelTuner; -import com.android.tv.MainActivity; -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.analytics.DurationTimer; -import com.android.tv.analytics.Tracker; -import com.android.tv.customization.CustomAction; -import com.android.tv.customization.TvCustomizationManager; -import com.android.tv.data.Channel; +import com.android.tv.menu.Menu.MenuShowReason; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** - * A subclass of VerticalGridView that shows TV main menu. + * A view that represents TV main menu. */ -public class MenuView extends FrameLayout implements OnChildSelectedListener { - static final String TAG = "MenuView"; +public class MenuView extends FrameLayout implements IMenuView { + static final String TAG = MenuView.class.getSimpleName(); static final boolean DEBUG = false; - // TODO: Change the status to STATUS_NONE when the animation for STATUS_CHILD_SELECTING - // is ended. - public static final int STATUS_CHILD_SELECTING = 3; - public static final String SCREEN_NAME = "Menu"; - - @Retention(RetentionPolicy.SOURCE) - @IntDef({REASON_NONE, REASON_GUIDE, REASON_PLAY_CONTROLS_PLAY, REASON_PLAY_CONTROLS_PAUSE, - REASON_PLAY_CONTROLS_PLAY_PAUSE, REASON_PLAY_CONTROLS_REWIND, - REASON_PLAY_CONTROLS_FAST_FORWARD, REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS, - REASON_PLAY_CONTROLS_JUMP_TO_NEXT}) - public @interface MenuShowReason {} - public static final int REASON_NONE = 0; - public static final int REASON_GUIDE = 1; - public static final int REASON_PLAY_CONTROLS_PLAY = 2; - public static final int REASON_PLAY_CONTROLS_PAUSE = 3; - public static final int REASON_PLAY_CONTROLS_PLAY_PAUSE = 4; - public static final int REASON_PLAY_CONTROLS_REWIND = 5; - public static final int REASON_PLAY_CONTROLS_FAST_FORWARD = 6; - public static final int REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS = 7; - public static final int REASON_PLAY_CONTROLS_JUMP_TO_NEXT = 8; - - public static final List sRowIdListForReason = new ArrayList<>(); - static { - sRowIdListForReason.add(null); // REASON_NONE - sRowIdListForReason.add(ChannelsRow.ID); // REASON_GUIDE - sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_PLAY - sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_PAUSE - sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_PLAY_PAUSE - sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_REWIND - sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_FAST_FORWARD - sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS - sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_JUMP_TO_NEXT - } - private final LayoutInflater mLayoutInflater; - private VerticalGridView mMenuList; - private final MenuAdapter mAdapter = new MenuAdapter(); - private ChannelTuner mChannelTuner; - private int mPreviousSelectedPosition; - - private Runnable mPreShowRunnable; - private Runnable mPostHideRunnable; - - private final Animator mShowAnimator; - private final Animator mHideAnimator; - private final int mMenuHeight; - private final int mMenuRowTitleHeight; - private final int mMenuRowPaddingHeight; - private final long mShowDurationMillis; - private final int mRowSelectionAnimationDurationMs; - private final OverScroller mScroller; - private final DurationTimer mVisibleTimer = new DurationTimer(); - - private ChannelsRow mChannelsRow; - - private Tracker mTracker; - - private boolean mKeepVisible; - @MenuShowReason private int mShowReason = REASON_NONE; + private final List mMenuRows = new ArrayList<>(); + private final List mMenuRowViews = new ArrayList<>(); - private final Runnable mHideRunnable = new Runnable() { - @Override - public void run() { - hide(true); - } - }; - - private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() { - @Override - public void onLoadFinished() {} - - @Override - public void onBrowsableChannelListChanged() { - update(); - } - - @Override - public void onCurrentChannelUnavailable(Channel channel) {} + @MenuShowReason private int mShowReason = Menu.REASON_NONE; - @Override - public void onChannelChanged(Channel previousChannel, Channel currentChannel) {} - }; + private final MenuLayoutManager mLayoutManager; public MenuView(Context context) { this(context, null, 0); @@ -150,516 +56,197 @@ public class MenuView extends FrameLayout implements OnChildSelectedListener { public MenuView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mLayoutInflater = LayoutInflater.from(context); - mShowAnimator = AnimatorInflater.loadAnimator(context, R.animator.menu_enter); - mShowAnimator.setTarget(this); - mHideAnimator = AnimatorInflater.loadAnimator(context, R.animator.menu_exit); - mHideAnimator.addListener(new AnimatorListenerAdapter() { + getViewTreeObserver().addOnGlobalFocusChangeListener(new OnGlobalFocusChangeListener() { @Override - public void onAnimationEnd(Animator animation) { - // Animation is still in running state at this point. - hideInternal(); + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + MenuRowView newParent = getParentMenuRowView(newFocus); + if (newParent != null) { + if (DEBUG) Log.d(TAG, "Focus changed to " + newParent); + // When the row is selected, the row view itself has the focus because the row + // is collapsed. To make the child of the row have the focus, requestFocus() + // should be called again after the row is expanded. It's done in + // setSelectedPosition(). + setSelectedPositionSmooth(mMenuRowViews.indexOf(newParent)); + } } }); - mHideAnimator.setTarget(this); - - Resources res = context.getResources(); - mShowDurationMillis = res.getInteger(R.integer.menu_show_duration); - mMenuHeight = res.getDimensionPixelSize(R.dimen.menu_height); - mMenuRowTitleHeight = res.getDimensionPixelSize(R.dimen.menu_row_title_height); - mMenuRowPaddingHeight = res.getDimensionPixelOffset(R.dimen.menu_list_padding_top) - + res.getDimensionPixelOffset(R.dimen.menu_list_padding_bottom) - + res.getDimensionPixelOffset(R.dimen.menu_list_margin_top); - mRowSelectionAnimationDurationMs = - res.getInteger(R.integer.menu_row_selection_anim_duration); - - mScroller = new OverScroller(context); + mLayoutManager = new MenuLayoutManager(context, this); } - private MainActivity getMainActivity() { - return (MainActivity) getContext(); - } - - /** - * This method will be called from MainActivity.onStart() - */ - public void onStart() { - Context context = getContext(); - - // Menu list(VerticalGridView) should be refreshed to forget the previous status. - // If not, mMenuList.setSelectedPosition() would not work properly. - mAdapter.notifyDataSetChanged(); - - MainActivity mainActivity = getMainActivity(); - mTracker= ((TvApplication) mainActivity.getApplication()).getTracker(); - - // Build menu rows - TvCustomizationManager manager = mainActivity.getTvCustomizationManager(); - List itemList = new ArrayList<>(); - itemList.add(new PlayControlsRow(context)); - itemList.add(mChannelsRow = new ChannelsRow(context)); - List customActions = - manager.getCustomActions(TvCustomizationManager.ID_PARTNER_ROW); - String title = manager.getPartnerRowTitle(); - if (customActions != null && !TextUtils.isEmpty(title)) { - itemList.add(new PartnerRow(context, title, customActions)); + @Override + public void setMenuRows(List menuRows) { + mMenuRows.clear(); + mMenuRows.addAll(menuRows); + for (MenuRow row : menuRows) { + MenuRowView view = createMenuRowView(row); + mMenuRowViews.add(view); + addView(view); } - itemList.add(new TvOptionsRow( - context, manager.getCustomActions(TvCustomizationManager.ID_OPTIONS_ROW))); - itemList.add(new PipOptionsRow(context)); - - mAdapter.setItemList(itemList); + mLayoutManager.setMenuRowsAndViews(mMenuRows, mMenuRowViews); } - /** - * This method will be called from MainActivity.onStop() - */ - public void onStop() { - mAdapter.resetItemList(); - } - - /** - * This method will be called when channels are updated. - */ - public void onRecentChannelUpdated() { - if (mChannelsRow != null) { - mChannelsRow.onRecentChannelUpdated(); - } + private MenuRowView createMenuRowView(MenuRow row) { + MenuRowView view = (MenuRowView) mLayoutInflater.inflate(row.getLayoutResId(), this, false); + view.onBind(row); + return view; } @Override - protected void onFinishInflate() { - mMenuList = (VerticalGridView) findViewById(R.id.menu_list); - mMenuList.setOnChildSelectedListener(this); - mMenuList.setScrollEnabled(false); - mMenuList.setAdapter(mAdapter); - // TODO: Use alignment features of GridView once the bugs of the features are fixed. - // NOTE: There's a problem that the menu jumps up/down, if a row whose position is less than - // the selected position is inserted or removed while the menu is displayed. - // The reason is because we use OverScroller to scroll the rows. - } - - public void setPreShowCallback(Runnable preShowRunnable) { - mPreShowRunnable = preShowRunnable; - } - - public void setPostHideCallback(Runnable postHideRunnable) { - mPostHideRunnable = postHideRunnable; - } - - public boolean isActive() { - return getVisibility() == View.VISIBLE && !isHiding(); + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + mLayoutManager.layout(left, top, right, bottom); } - public boolean isHiding() { - return mHideAnimator.isStarted(); - } - - /** - * Returns the padding to the height of the item. - * - *

It is used to calculate the exact height of the item. - */ - public int getItemPaddingHeight() { - return mMenuRowPaddingHeight; - } - - /** - * Shows the main menu. - * - * @param reason A reason why this is called. See {@link MenuShowReason} - */ - public void show(@MenuShowReason int reason) { - if (DEBUG) Log.d(TAG, "show reason:" + reason); - mTracker.sendShowMenu(); - mVisibleTimer.start(); - mShowReason = reason; - if (isHiding()) { - mHideAnimator.end(); + @Override + public void onShow(@MenuShowReason int reason, String rowIdToSelect, + final Runnable runnableAfterShow) { + if (DEBUG) { + Log.d(TAG, "onShow(reason=" + reason + ", rowIdToSelect=" + rowIdToSelect + ")"); } - String rowIdToSelect = sRowIdListForReason.get(reason); - if (getVisibility() == View.VISIBLE) { + mShowReason = reason; + if (getVisibility() == VISIBLE) { if (rowIdToSelect != null) { - int position = mAdapter.getItemPosition(rowIdToSelect); + int position = getItemPosition(rowIdToSelect); if (position >= 0) { - for (int i = 0; i < mMenuList.getChildCount(); ++i) { - MenuRowView rowView = (MenuRowView) mMenuList.getChildAt(i); - if (rowIdToSelect.equals(rowView.getRowId())) { - rowView.initialize(reason); - break; - } - } - mMenuList.setSelectedPosition(position); - requestFocus(); + MenuRowView rowView = mMenuRowViews.get(position); + rowView.initialize(reason); + setSelectedPosition(position); } } return; } + initializeChildren(); + update(true); if (rowIdToSelect == null) { rowIdToSelect = ChannelsRow.ID; } - // The child row views need be initialized before they become visible. - initializeChildren(); - setVisibility(View.VISIBLE); - mTracker.sendScreenView(SCREEN_NAME); - if (mPreShowRunnable != null) { - mPreShowRunnable.run(); - } - if (update()) { - // To apply the row insertion or removal immediately, - // notifyDataSetChanged need to be called after update. - // If we don't call this, the intermediate state might be shown. - mAdapter.notifyDataSetChanged(); - } - int positionToSelect = mAdapter.getItemPosition(rowIdToSelect); - resetSelectedItemPosition(positionToSelect); + int position = getItemPosition(rowIdToSelect); + setSelectedPosition(position); + // Change the visibility as late as possible to avoid the unnecessary animation. + setVisibility(VISIBLE); + // Make the selected row have the focus. requestFocus(); - - // Abort animation because the scroll animation can occur while updating the adapter above. - mScroller.abortAnimation(); - setScrollY(getScrollPosition(positionToSelect)); - mShowAnimator.start(); - scheduleHide(); - } - - int getItemPositionY(int position) { - return mMenuHeight - mMenuRowTitleHeight - mAdapter.getItemHeight(position); - } - - private void initializeChildren() { - for (int i = 0, count = mMenuList.getChildCount(); i < count; ++i) { - MenuRowView rowView = (MenuRowView) mMenuList.getChildAt(i); - rowView.initialize(mShowReason); - } - } - - private void resetSelectedItemPosition(int positionToSelect) { - mPreviousSelectedPosition = positionToSelect; - if (DEBUG) Log.d(TAG, "Row count of the main menu is " + mMenuList.getChildCount()); - /* - * Must reset mMenuList's selected position after resetting selected position of child - * ListView. Otherwise it can be changed while resetting child ListView. - */ - mMenuList.setSelectedPosition(mPreviousSelectedPosition); - for (int i = 0, count = mMenuList.getChildCount(); i < count; ++i) { - MenuRowView rowView = (MenuRowView) mMenuList.getChildAt(i); - if (DEBUG) { - Log.d(TAG, "The child position of the row " + i + " is " - + mMenuList.getChildAdapterPosition(rowView)); - } - rowView.updateView(false); + if (runnableAfterShow != null) { + runnableAfterShow.run(); } + mLayoutManager.onMenuShow(); } - public void hide(boolean withAnimation) { - removeCallbacks(mHideRunnable); - if (withAnimation) { - if (!isHiding()) { - mHideAnimator.start(); - } - return; - } - if (isHiding()) { - mHideAnimator.end(); - return; - } - hideInternal(); - } - - private void hideInternal() { - if (getVisibility() == View.GONE) { + @Override + public void onHide() { + if (getVisibility() == GONE) { return; } - mTracker.sendHideMenu(mVisibleTimer.reset()); - setVisibility(View.GONE); - if (mPostHideRunnable != null) { - mPostHideRunnable.run(); - } - } - - public void scheduleHide() { - removeCallbacks(mHideRunnable); - if (!mKeepVisible) { - postDelayed(mHideRunnable, mShowDurationMillis); - } + mLayoutManager.onMenuHide(); + setVisibility(GONE); } - /** - * Called when the caller wants the main menu to be kept visible or not. - * If {@code keepVisible} is set to {@code true}, the hide schedule doesn't close the main menu, - * but calling {@link #hide} still hides it. - * If {@code keepVisible} is set to {@code false}, the hide schedule works as usual. - */ - public void setKeepVisible(boolean keepVisible) { - mKeepVisible = keepVisible; - if (mKeepVisible) { - removeCallbacks(mHideRunnable); - } else if (isActive()) { - scheduleHide(); - } - } - - public void setChannelTuner(ChannelTuner channelTuner) { - if (mChannelTuner != null) { - mChannelTuner.removeListener(mChannelTunerListener); - } - mChannelTuner = channelTuner; - if (mChannelTuner != null) { - mChannelTuner.addListener(mChannelTunerListener); - } - update(); - } - - /** - * Updates the options row. - */ - public void updateOptionsRow() { - if (DEBUG) { - Log.d(TAG, "update options row in main menu"); - } - mAdapter.updateOptionsRow(); + @Override + public boolean isVisible() { + return getVisibility() == VISIBLE; } - /** - * Updates the adapter. - * - *

Returns <@code true> if the adapter has been changed, otherwise {@code false}. - */ - public boolean update() { - if (DEBUG) { - Log.d(TAG, "update main menu"); + @Override + public boolean update(boolean menuActive) { + if (menuActive) { + for (MenuRow row : mMenuRows) { + row.update(); + } + mLayoutManager.onMenuRowUpdated(); + return true; } - return mAdapter.update(); - } - - /** - * Returns a duration of the animation when the row selection changes. - */ - public int getRowSelectionAnimationDurationMs() { - return mRowSelectionAnimationDurationMs; + return false; } @Override - public void computeScroll() { - super.computeScroll(); - if (mScroller.computeScrollOffset()) { - setScrollY(mScroller.getCurrY()); - invalidate(); + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + int selectedPosition = mLayoutManager.getSelectedPosition(); + // When the menu shows up, the selected row should have focus. + if (selectedPosition >= 0 && selectedPosition < mMenuRowViews.size()) { + return mMenuRowViews.get(selectedPosition).requestFocus(); } + return super.onRequestFocusInDescendants(direction, previouslyFocusedRect); } - private boolean scrollYSmooth(int position) { - int newScrollPosition = getScrollPosition(position); - if (newScrollPosition == getScrollY()) { - return false; - } - mScroller.startScroll(0, getScrollY(), 0, newScrollPosition - getScrollY(), - mRowSelectionAnimationDurationMs); - return true; + private void setSelectedPosition(int position) { + mLayoutManager.setSelectedPosition(position); } - private int getScrollPosition(int selectedPosition) { - int visibleHeight = mMenuRowTitleHeight * selectedPosition - + mAdapter.getItemHeight(selectedPosition); - boolean lastItem = selectedPosition == mAdapter.getItemCount() - 1; - if (!lastItem) { - visibleHeight += mMenuRowTitleHeight; - } - return visibleHeight - mMenuHeight; + private void setSelectedPositionSmooth(int position) { + mLayoutManager.setSelectedPositionSmooth(position); } - @Override - public void onChildSelected(ViewGroup parent, View child, int position, long id) { - boolean withAnimation = mPreviousSelectedPosition != position; - for (int i = 0; i < mMenuList.getChildCount(); i++) { - MenuRowView rowView = (MenuRowView) mMenuList.getChildAt(i); - rowView.updateView(withAnimation); - } - mPreviousSelectedPosition = position; - if (withAnimation) { - mScroller.abortAnimation(); - scrollYSmooth(position); + private void initializeChildren() { + for (MenuRowView view : mMenuRowViews) { + view.initialize(mShowReason); } } - /** - * Returns the previous selected position. - */ - public int getPreviousSelectedPosition() { - return mPreviousSelectedPosition; - } - - private class MenuAdapter extends RecyclerView.Adapter { - private List mAllItems = Collections.emptyList(); - private List mVisibleItems = new ArrayList<>(); - - private void setItemList(List items) { - mAllItems = items; - updateVisibleItems(); + private int getItemPosition(String rowIdToSelect) { + if (rowIdToSelect == null) { + return -1; } - - private void resetItemList() { - for (MenuRow item : mAllItems) { - item.release(); + int position = 0; + for (MenuRow item : mMenuRows) { + if (rowIdToSelect.equals(item.getId())) { + return position; } - setItemList(Collections.emptyList()); + ++position; } + return -1; + } - private void updateOptionsRow() { - if (isActive()) { - for (MenuRow item : mAllItems) { - if (item.getId().equals(TvOptionsRow.ID)) { - item.update(); + @Override + public View focusSearch(View focused, int direction) { + // The bounds of the views move and overlap with each other during the animation. In this + // situation, the framework can't perform the correct focus navigation. So the menu view + // should search by itself. + if (direction == View.FOCUS_UP) { + View newView = super.focusSearch(focused, direction); + MenuRowView oldfocusedParent = getParentMenuRowView(focused); + MenuRowView newFocusedParent = getParentMenuRowView(newView); + int selectedPosition = mLayoutManager.getSelectedPosition(); + if (newFocusedParent != oldfocusedParent) { + // The focus leaves from the current menu row view. + for (int i = selectedPosition - 1; i >= 0; --i) { + MenuRowView view = mMenuRowViews.get(i); + if (view.getVisibility() == View.VISIBLE) { + return view; } } } - } - - private boolean update() { - if (isActive()) { - for (MenuRow item : mAllItems) { - item.update(); - } - return updateVisibleItems(); - } - return false; - } - - private boolean updateVisibleItems() { - // To preserve the item focus, we need a fine-grained control using notifyItemXXXed() - // instead of using notifyDataSetChanged(). - // We assume that the order of the adapters will not be changed. - List oldVisibleItems = mVisibleItems; - mVisibleItems = new ArrayList<>(); - boolean changed = false; - int oldSelectedPosition = mMenuList.getSelectedPosition(); - MenuRow oldSelectedRow = null; - if (oldSelectedPosition >= 0 && oldSelectedPosition < oldVisibleItems.size()) { - oldSelectedRow = oldVisibleItems.get(oldSelectedPosition); - } - int position = 0; - int newSelectedPosition = 0; - for (MenuRow item : mAllItems) { - if (item.isVisible()) { - mVisibleItems.add(item); - if (!oldVisibleItems.contains(item)) { - notifyItemInserted(position); - changed = true; + return newView; + } else if (direction == View.FOCUS_DOWN) { + View newView = super.focusSearch(focused, direction); + MenuRowView oldfocusedParent = getParentMenuRowView(focused); + MenuRowView newFocusedParent = getParentMenuRowView(newView); + int selectedPosition = mLayoutManager.getSelectedPosition(); + if (newFocusedParent != oldfocusedParent) { + // The focus leaves from the current menu row view. + int count = mMenuRowViews.size(); + for (int i = selectedPosition + 1; i < count; ++i) { + MenuRowView view = mMenuRowViews.get(i); + if (view.getVisibility() == View.VISIBLE) { + return view; } - if (item.equals(oldSelectedRow)) { - newSelectedPosition = position; - } - ++position; - } else if (oldVisibleItems.contains(item)) { - notifyItemRemoved(position); - changed = true; - } - } - if (DEBUG) Log.d(TAG, "Visible item count is " + mVisibleItems.size()); - if (changed && scrollYSmooth(newSelectedPosition)) { - // Call invalidate() to make sure that computeScroll() is invoked. - invalidate(); - } - return changed; - } - - @Override - public int getItemViewType(int position) { - // Each row needs to have a unique view type to avoid messing the focus up. - // If a row is recycled from a view of another type, the previous focus will not be - // preserved. - return mVisibleItems.get(position).getId().hashCode(); - } - - @Override - public MenuViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - for (MenuRow item : mVisibleItems) { - if (viewType == item.getId().hashCode()) { - MenuRowView view = (MenuRowView) mLayoutInflater.inflate(item.getLayoutResId(), - parent, false); - view.setMenuView(MenuView.this); - view.setParentView(mMenuList); - return new MenuViewHolder(view); - } - } - // Main menu is in the illegal state. - Log.e(TAG, "Error in creating view holder", new IllegalStateException( - "Can't create view holder due to the invalid view type " + viewType)); - return null; - } - - @Override - public void onBindViewHolder(MenuViewHolder viewHolder, int position) { - MenuRowView itemView = (MenuRowView) viewHolder.itemView; - MenuRow item = mVisibleItems.get(position); - itemView.onBind(item); - itemView.initialize(mShowReason); - } - - @Override - public int getItemCount() { - return mVisibleItems.size(); - } - - private int getItemPosition(String rowIdToSelect) { - if (rowIdToSelect == null) { - return -1; - } - int position = 0; - for (MenuRow item : mVisibleItems) { - if (rowIdToSelect.equals(item.getId())) { - return position; } - ++position; - } - return -1; - } - - private int getItemHeight(int position) { - if (position < 0 || position >= mVisibleItems.size()) { - return mMenuRowTitleHeight; } - return mVisibleItems.get(position).getHeight() + mMenuRowPaddingHeight - + mMenuRowTitleHeight; + return newView; } + return super.focusSearch(focused, direction); } - private static class MenuViewHolder extends RecyclerView.ViewHolder { - MenuViewHolder(View view) { - super(view); - } - } - - private static class TvOptionsRow extends ItemListRow { - private static final String ID = TvOptionsRow.class.getName(); - public TvOptionsRow(Context context, List customActions) { - super(context, R.string.menu_title_options, R.dimen.action_card_height, - new TvOptionsRowAdapter(context, customActions)); - } - - @Override - public String getId() { - return ID; - } - } - - private static class PipOptionsRow extends ItemListRow { - public PipOptionsRow(Context context) { - super(context, R.string.menu_title_pip_options, R.dimen.action_card_height, - new PipOptionsRowAdapter(context)); + private MenuRowView getParentMenuRowView(View view) { + if (view == null) { + return null; } - - @Override - public boolean isVisible() { - return super.isVisible() && getMainActivity().isPipEnabled(); + ViewParent parent = view.getParent(); + if (parent == MenuView.this) { + return (MenuRowView) view; } - } - - private static class PartnerRow extends ItemListRow { - public PartnerRow(Context context, String title, List customActions) { - super(context, title, R.dimen.action_card_height, - new PartnerOptionsRowAdapter(context, customActions)); + if (parent instanceof View) { + return getParentMenuRowView((View) parent); } + return null; } } diff --git a/src/com/android/tv/menu/PlayControlsRow.java b/src/com/android/tv/menu/PlayControlsRow.java index 442407df..588ecf6a 100644 --- a/src/com/android/tv/menu/PlayControlsRow.java +++ b/src/com/android/tv/menu/PlayControlsRow.java @@ -20,13 +20,15 @@ import android.content.Context; import com.android.tv.R; import com.android.tv.TimeShiftManager; -import com.android.tv.common.TvCommonConstants; public class PlayControlsRow extends MenuRow { public static final String ID = PlayControlsRow.class.getName(); - public PlayControlsRow(Context context) { - super(context, R.string.menu_title_play_controls, R.dimen.play_controls_height); + private final TimeShiftManager mTimeShiftManager; + + public PlayControlsRow(Context context, Menu menu, TimeShiftManager timeShiftManager) { + super(context, menu, R.string.menu_title_play_controls, R.dimen.play_controls_height); + mTimeShiftManager = timeShiftManager; } @Override @@ -38,8 +40,11 @@ public class PlayControlsRow extends MenuRow { return R.layout.play_controls; } + /** + * Returns an instance of {@link TimeShiftManager}. + */ public TimeShiftManager getTimeShiftManager() { - return getMainActivity().getTimeShiftManager(); + return mTimeShiftManager; } @Override @@ -49,6 +54,11 @@ public class PlayControlsRow extends MenuRow { @Override public boolean isVisible() { - return TvCommonConstants.HAS_TIME_SHIFT_API; + return mTimeShiftManager.isAvailable(); + } + + @Override + public boolean hideTitleWhenSelected() { + return true; } } diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java index 96b0ece3..d4ad7877 100644 --- a/src/com/android/tv/menu/PlayControlsRowView.java +++ b/src/com/android/tv/menu/PlayControlsRowView.java @@ -28,7 +28,7 @@ import com.android.tv.R; import com.android.tv.TimeShiftManager; import com.android.tv.TimeShiftManager.TimeShiftActionId; import com.android.tv.data.Program; -import com.android.tv.menu.MenuView.MenuShowReason; +import com.android.tv.menu.Menu.MenuShowReason; public class PlayControlsRowView extends MenuRowView { // Dimensions @@ -36,7 +36,6 @@ public class PlayControlsRowView extends MenuRowView { private final int mTimeTextLeftMargin; private final int mTimelineWidth; // Views - private View mTitleView; private View mBackgroundView; private View mTimeIndicator; private TextView mTimeText; @@ -91,7 +90,6 @@ public class PlayControlsRowView extends MenuRowView { super.onFinishInflate(); // Clip the ViewGroup(body) to the rounded rectangle of outline. findViewById(R.id.body).setClipToOutline(true); - mTitleView = findViewById(R.id.title); mBackgroundView = findViewById(R.id.background); mTimeIndicator = findViewById(R.id.time_indicator); mTimeText = (TextView) findViewById(R.id.time_text); @@ -159,18 +157,6 @@ public class PlayControlsRowView extends MenuRowView { } } }); - changeFocusableForDescendents(false); - } - - private void changeFocusableForDescendents(boolean focusable) { - setFocusable(focusable); - setDescendantFocusability(focusable ? FOCUS_AFTER_DESCENDANTS : FOCUS_BLOCK_DESCENDANTS); - } - - private void setRowEnable(boolean enable) { - setEnabled(enable); - changeFocusableForDescendents(enable); - mTitleView.setVisibility(enable ? View.VISIBLE : View.INVISIBLE); } private void initializeButton(PlayControlsButton button, int imageResId, @@ -250,11 +236,11 @@ public class PlayControlsRowView extends MenuRowView { private void onAvailabilityChanged() { if (mTimeShiftManager.isAvailable()) { - setRowEnable(true); + setEnabled(true); initializeTimeline(); mBackgroundView.setEnabled(true); } else { - setRowEnable(false); + setEnabled(false); mBackgroundView.setEnabled(false); } updateAll(); @@ -269,31 +255,21 @@ public class PlayControlsRowView extends MenuRowView { private void updateMenuVisibility() { boolean keepMenuVisible = mTimeShiftManager.isAvailable() && !mTimeShiftManager.isNormalPlaying(); - getMenuView().setKeepVisible(keepMenuVisible); + getMenu().setKeepVisible(keepMenuVisible); } @Override - public void updateView(boolean withAnimation) { - super.updateView(withAnimation); + public void onSelected(boolean showTitle) { + super.onSelected(showTitle); updateAll(); postHideRippleAnimation(); } - @Override - protected float getTitleScaleSelected() { - return 1.0f; - } - - @Override - protected float getTitleAlphaSelected() { - return 0.0f; - } - @Override public void initialize(@MenuShowReason int reason) { super.initialize(reason); switch (reason) { - case MenuView.REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS: + case Menu.REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS: if (mTimeShiftManager.isActionEnabled( TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS)) { setInitialFocusView(mJumpPreviousButton); @@ -301,7 +277,7 @@ public class PlayControlsRowView extends MenuRowView { setInitialFocusView(mPlayPauseButton); } break; - case MenuView.REASON_PLAY_CONTROLS_REWIND: + case Menu.REASON_PLAY_CONTROLS_REWIND: if (mTimeShiftManager.isActionEnabled( TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND)) { setInitialFocusView(mRewindButton); @@ -309,7 +285,7 @@ public class PlayControlsRowView extends MenuRowView { setInitialFocusView(mPlayPauseButton); } break; - case MenuView.REASON_PLAY_CONTROLS_FAST_FORWARD: + case Menu.REASON_PLAY_CONTROLS_FAST_FORWARD: if (mTimeShiftManager.isActionEnabled( TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD)) { setInitialFocusView(mFastForwardButton); @@ -317,7 +293,7 @@ public class PlayControlsRowView extends MenuRowView { setInitialFocusView(mPlayPauseButton); } break; - case MenuView.REASON_PLAY_CONTROLS_JUMP_TO_NEXT: + case Menu.REASON_PLAY_CONTROLS_JUMP_TO_NEXT: if (mTimeShiftManager.isActionEnabled( TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT)) { setInitialFocusView(mJumpNextButton); @@ -325,9 +301,9 @@ public class PlayControlsRowView extends MenuRowView { setInitialFocusView(mPlayPauseButton); } break; - case MenuView.REASON_PLAY_CONTROLS_PLAY_PAUSE: - case MenuView.REASON_PLAY_CONTROLS_PLAY: - case MenuView.REASON_PLAY_CONTROLS_PAUSE: + case Menu.REASON_PLAY_CONTROLS_PLAY_PAUSE: + case Menu.REASON_PLAY_CONTROLS_PLAY: + case Menu.REASON_PLAY_CONTROLS_PAUSE: default: setInitialFocusView(mPlayPauseButton); break; diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java index 5b203551..b7814fa5 100644 --- a/src/com/android/tv/menu/TvOptionsRowAdapter.java +++ b/src/com/android/tv/menu/TvOptionsRowAdapter.java @@ -18,6 +18,7 @@ package com.android.tv.menu; import android.content.Context; import android.media.tv.TvTrackInfo; +import android.support.annotation.VisibleForTesting; import com.android.tv.R; import com.android.tv.TvOptionsManager; @@ -138,7 +139,8 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { return changed; } - private boolean updateMultiAudioAction() { + @VisibleForTesting + boolean updateMultiAudioAction() { List audioTracks = getMainActivity().getTracks(TvTrackInfo.TYPE_AUDIO); boolean oldEnabled = MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled(); boolean newEnabled = audioTracks != null && audioTracks.size() > 1; diff --git a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java index ca3cb176..949222a9 100644 --- a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java +++ b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java @@ -36,6 +36,13 @@ public final class AudioCapabilitiesReceiver { private static final String PREFS_NAME = "com.android.tv.audio_capabilities"; private static final String SETTINGS_KEY_AC3_PASSTHRU_REPORTED = "ac3_passthrough_reported"; private static final String SETTINGS_KEY_AC3_PASSTHRU_CAPABILITIES = "ac3_passthrough"; + private static final String SETTINGS_KEY_AC3_REPORT_REVISION = "ac3_report_revision"; + + // AC3 capabilities stat is sent to Google Analytics just once in order to avoid + // duplicated stat reports since it doesn't change over time in most cases. + // Increase this revision when we should force the stat to be sent again. + // TODO: Consier using custom metrics. + private static final int REPORT_REVISION = 1; private final Context mContext; private final Tracker mTracker; @@ -72,17 +79,21 @@ public final class AudioCapabilitiesReceiver { } private void reportAudioCapabilities(int[] supportedEncodings) { - boolean newVal = supportedEncodings == null - ? false : Arrays.binarySearch(supportedEncodings, AudioFormat.ENCODING_AC3) >= 0; - boolean oldVal = getBoolean(SETTINGS_KEY_AC3_PASSTHRU_REPORTED, false); - boolean reported = getBoolean(SETTINGS_KEY_AC3_PASSTHRU_CAPABILITIES, false); + boolean newVal = supportedEncodings != null + && Arrays.binarySearch(supportedEncodings, AudioFormat.ENCODING_AC3) >= 0; + boolean oldVal = getBoolean(SETTINGS_KEY_AC3_PASSTHRU_CAPABILITIES, false); + boolean reported = getBoolean(SETTINGS_KEY_AC3_PASSTHRU_REPORTED, false); + int revision = getInt(SETTINGS_KEY_AC3_REPORT_REVISION, 0); // Send the value just once. But we send it again if the value changed, to include // the case where users have switched TV device with different AC3 passthrough capabilities. - if (!reported || oldVal != newVal) { + if (!reported || oldVal != newVal || REPORT_REVISION > revision) { mTracker.sendAc3PassthroughCapabilities(newVal); setBoolean(SETTINGS_KEY_AC3_PASSTHRU_REPORTED, true); setBoolean(SETTINGS_KEY_AC3_PASSTHRU_CAPABILITIES, newVal); + if (REPORT_REVISION > revision) { + setInt(SETTINGS_KEY_AC3_REPORT_REVISION, REPORT_REVISION); + } } } @@ -97,4 +108,12 @@ public final class AudioCapabilitiesReceiver { private void setBoolean(String key, boolean val) { getSharedPreferences().edit().putBoolean(key, val).apply(); } + + private int getInt(String key, int def) { + return getSharedPreferences().getInt(key, def); + } + + private void setInt(String key, int val) { + getSharedPreferences().edit().putInt(key, val).apply(); + } } diff --git a/src/com/android/tv/recommendation/NotificationService.java b/src/com/android/tv/recommendation/NotificationService.java index 00cad116..835a3e53 100644 --- a/src/com/android/tv/recommendation/NotificationService.java +++ b/src/com/android/tv/recommendation/NotificationService.java @@ -31,13 +31,17 @@ import android.media.tv.TvInputInfo; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; +import android.os.Looper; import android.os.Message; +import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; import android.util.SparseLongArray; import android.view.View; import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.util.BitmapUtils; @@ -59,8 +63,13 @@ public class NotificationService extends Service implements Recommender.Listener public static final String ACTION_HIDE_RECOMMENDATION = "com.android.tv.notification.ACTION_HIDE_RECOMMENDATION"; - private static final String TUNE_PARAMS_RECOMMENDATION_TYPE = + /** + * Recommendation intent has an extra data for the recommendation type. It'll be also + * sent to a TV input as a tune parameter. + */ + public static final String TUNE_PARAMS_RECOMMENDATION_TYPE = "com.android.tv.recommendation_type"; + private static final String TYPE_RANDOM_RECOMMENDATION = "random"; private static final String TYPE_ROUTINE_WATCH_RECOMMENDATION = "routine_watch"; private static final String TYPE_ROUTINE_WATCH_AND_FAVORITE_CHANNEL_RECOMMENDATION = @@ -132,66 +141,52 @@ public class NotificationService extends Service implements Recommender.Listener getResources().getDimensionPixelOffset(R.dimen.notif_ch_logo_padding_bottom); mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - mTvInputManagerHelper = new TvInputManagerHelper(this); - mTvInputManagerHelper.start(); - + mTvInputManagerHelper = ((TvApplication) getApplicationContext()).getTvInputManagerHelper(); mHandlerThread = new HandlerThread("tv notification"); mHandlerThread.start(); - mHandler = new Handler(mHandlerThread.getLooper()) { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_INITIALIZE_RECOMMENDER: { - mRecommender = new Recommender( - NotificationService.this, NotificationService.this, true); - if (TYPE_RANDOM_RECOMMENDATION.equals(mRecommendationType)) { - mRecommender.registerEvaluator(new RandomEvaluator()); - } else if (TYPE_ROUTINE_WATCH_RECOMMENDATION.equals(mRecommendationType)) { - mRecommender.registerEvaluator(new RoutineWatchEvaluator()); - } else if (TYPE_ROUTINE_WATCH_AND_FAVORITE_CHANNEL_RECOMMENDATION.equals( - mRecommendationType)) { - mRecommender.registerEvaluator( - new FavoriteChannelEvaluator(), 0.5, 0.5); - mRecommender.registerEvaluator(new RoutineWatchEvaluator(), 1.0, 1.0); - } else { - throw new IllegalStateException("Undefined recommendation type: " - + mRecommendationType); - } - } - case MSG_SHOW_RECOMMENDATION: { - if (!mRecommender.isReady()) { - mShowRecommendationAfterRecommenderReady = true; - } else { - showRecommendation(); - } - break; - } - case MSG_UPDATE_RECOMMENDATION: { - int notificationId = msg.arg1; - Channel channel = ((Channel) msg.obj); - if (mNotificationChannels[notificationId] == Channel.INVALID_ID - || !sendNotification(channel.getId(), notificationId)) { - changeRecommendation(notificationId); - } - break; - } - case MSG_HIDE_RECOMMENDATION: { - if (!mRecommender.isReady()) { - mShowRecommendationAfterRecommenderReady = false; - } else { - hideAllRecommendation(); - } - break; - } - default: { - super.handleMessage(msg); - } - } - } - }; + mHandler = new NotificationHandler(mHandlerThread.getLooper(), this); mHandler.sendEmptyMessage(MSG_INITIALIZE_RECOMMENDER); } + private void handleInitializeRecommender() { + mRecommender = new Recommender(NotificationService.this, NotificationService.this, true); + if (TYPE_RANDOM_RECOMMENDATION.equals(mRecommendationType)) { + mRecommender.registerEvaluator(new RandomEvaluator()); + } else if (TYPE_ROUTINE_WATCH_RECOMMENDATION.equals(mRecommendationType)) { + mRecommender.registerEvaluator(new RoutineWatchEvaluator()); + } else if (TYPE_ROUTINE_WATCH_AND_FAVORITE_CHANNEL_RECOMMENDATION + .equals(mRecommendationType)) { + mRecommender.registerEvaluator(new FavoriteChannelEvaluator(), 0.5, 0.5); + mRecommender.registerEvaluator(new RoutineWatchEvaluator(), 1.0, 1.0); + } else { + throw new IllegalStateException( + "Undefined recommendation type: " + mRecommendationType); + } + } + + private void handleShowRecommendation() { + if (!mRecommender.isReady()) { + mShowRecommendationAfterRecommenderReady = true; + } else { + showRecommendation(); + } + } + + private void handleUpdateRecommendation(int notificationId, Channel channel) { + if (mNotificationChannels[notificationId] == Channel.INVALID_ID || !sendNotification( + channel.getId(), notificationId)) { + changeRecommendation(notificationId); + } + } + + private void handleHideRecommendation() { + if (!mRecommender.isReady()) { + mShowRecommendationAfterRecommenderReady = false; + } else { + hideAllRecommendation(); + } + } + @Override public void onDestroy() { mRecommender.release(); @@ -456,4 +451,37 @@ public class NotificationService extends Service implements Recommender.Listener } return -1; } + + private static class NotificationHandler extends WeakHandler { + public NotificationHandler(@NonNull Looper looper, NotificationService ref) { + super(looper, ref); + } + + @Override + public void handleMessage(Message msg, @NonNull NotificationService notificationService) { + switch (msg.what) { + case MSG_INITIALIZE_RECOMMENDER: { + notificationService.handleInitializeRecommender(); + break; + } + case MSG_SHOW_RECOMMENDATION: { + notificationService.handleShowRecommendation(); + break; + } + case MSG_UPDATE_RECOMMENDATION: { + int notificationId = msg.arg1; + Channel channel = ((Channel) msg.obj); + notificationService.handleUpdateRecommendation(notificationId, channel); + break; + } + case MSG_HIDE_RECOMMENDATION: { + notificationService.handleHideRecommendation(); + break; + } + default: { + super.handleMessage(msg); + } + } + } + } } diff --git a/src/com/android/tv/recommendation/RecommendationDataManager.java b/src/com/android/tv/recommendation/RecommendationDataManager.java index 2445cce8..0f59e2bd 100644 --- a/src/com/android/tv/recommendation/RecommendationDataManager.java +++ b/src/com/android/tv/recommendation/RecommendationDataManager.java @@ -28,9 +28,12 @@ import android.media.tv.TvInputManager.TvInputCallback; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; +import android.os.Looper; import android.os.Message; import android.support.annotation.NonNull; +import android.support.annotation.WorkerThread; +import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; import com.android.tv.data.Program; @@ -71,11 +74,12 @@ public class RecommendationDataManager { private static final int INVALID_INDEX = -1; private static RecommendationDataManager sManager; + private final static Object sListenerLock = new Object(); private final ContentObserver mContentObserver; private final Map mChannelRecordMap = new ConcurrentHashMap<>(); private final Map mAvailableChannelRecordMap = new ConcurrentHashMap<>(); - private Context mContext; + private final Context mContext; private boolean mStarted; private boolean mCancelLoadTask; private boolean mChannelRecordMapLoaded; @@ -90,7 +94,6 @@ public class RecommendationDataManager { private final HandlerThread mHandlerThread; - @SuppressWarnings("unchecked") private final Handler mHandler; private final List mListeners = new ArrayList<>(); @@ -117,7 +120,7 @@ public class RecommendationDataManager { */ public void release(@NonNull Listener listener) { removeListener(listener); - synchronized (mListeners) { + synchronized (sListenerLock) { if (mListeners.size() == 0) { stop(); } @@ -183,46 +186,7 @@ public class RecommendationDataManager { mContext = context.getApplicationContext(); mHandlerThread = new HandlerThread("RecommendationDataManager"); mHandlerThread.start(); - mHandler = new Handler(mHandlerThread.getLooper()) { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_START: - onStart(); - break; - case MSG_STOP: - if (mStarted) { - onStop(); - } - break; - case MSG_UPDATE_CHANNEL: - if (mStarted) { - onUpdateChannel((Uri) msg.obj); - } - break; - case MSG_UPDATE_CHANNELS: - if (mStarted) { - onUpdateChannels((Uri) msg.obj); - } - break; - case MSG_UPDATE_WATCH_HISTORY: - if (mStarted) { - onLoadWatchHistory((Uri) msg.obj); - } - break; - case MSG_NOTIFY_CHANNEL_RECORD_MAP_LOADED: - if (mStarted) { - onNotifyChannelRecordMapLoaded(); - } - break; - case MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED: - if (mStarted) { - onNotifyChannelRecordMapChanged(); - } - break; - } - } - }; + mHandler = new RecommendationHandler(mHandlerThread.getLooper(), this); mContentObserver = new RecommendationContentObserver(mHandler); } @@ -270,7 +234,7 @@ public class RecommendationDataManager { } private void addListener(Listener listener) { - synchronized (mListeners) { + synchronized (sListenerLock) { if (getListenerIndexLocked(listener) == INVALID_INDEX) { mListeners.add((new ListenerRecord(listener))); } @@ -278,10 +242,11 @@ public class RecommendationDataManager { } private void removeListener(Listener listener) { - synchronized (mListeners) { + synchronized (sListenerLock) { int idx = getListenerIndexLocked(listener); if (idx != INVALID_INDEX) { - mListeners.remove(idx); + ListenerRecord record = mListeners.remove(idx); + record.mListener = null; } } } @@ -319,6 +284,7 @@ public class RecommendationDataManager { mStarted = false; } + @WorkerThread private void onUpdateChannel(Uri uri) { Channel channel = null; try (Cursor cursor = mContext.getContentResolver().query(uri, Channel.PROJECTION, @@ -341,6 +307,7 @@ public class RecommendationDataManager { } } + @WorkerThread private void onUpdateChannels(Uri uri) { List channels = new ArrayList<>(); try (Cursor cursor = mContext.getContentResolver().query(uri, Channel.PROJECTION, @@ -378,6 +345,7 @@ public class RecommendationDataManager { } } + @WorkerThread private void onLoadWatchHistory(Uri uri) { List history = new ArrayList<>(); try (Cursor cursor = mContext.getContentResolver().query(uri, null, null, null, null)) { @@ -394,7 +362,7 @@ public class RecommendationDataManager { final ChannelRecord channelRecord = updateChannelRecordFromWatchedProgram(watchedProgram); if (mChannelRecordMapLoaded && channelRecord != null) { - synchronized (mListeners) { + synchronized (sListenerLock) { for (ListenerRecord l : mListeners) { l.postNewWatchLog(channelRecord); } @@ -437,7 +405,7 @@ public class RecommendationDataManager { private void onNotifyChannelRecordMapLoaded() { mChannelRecordMapLoaded = true; - synchronized (mListeners) { + synchronized (sListenerLock) { for (ListenerRecord l : mListeners) { l.postChannelRecordLoaded(); } @@ -445,7 +413,7 @@ public class RecommendationDataManager { } private void onNotifyChannelRecordMapChanged() { - synchronized (mListeners) { + synchronized (sListenerLock) { for (ListenerRecord l : mListeners) { l.postChannelRecordChanged(); } @@ -551,8 +519,10 @@ public class RecommendationDataManager { mHandler.post(new Runnable() { @Override public void run() { - if (mListener != null) { - mListener.onChannelRecordLoaded(); + synchronized (sListenerLock) { + if (mListener != null) { + mListener.onChannelRecordLoaded(); + } } } }); @@ -562,8 +532,10 @@ public class RecommendationDataManager { mHandler.post(new Runnable() { @Override public void run() { - if (mListener != null) { - mListener.onNewWatchLog(channelRecord); + synchronized (sListenerLock) { + if (mListener != null) { + mListener.onNewWatchLog(channelRecord); + } } } }); @@ -573,11 +545,58 @@ public class RecommendationDataManager { mHandler.post(new Runnable() { @Override public void run() { - if (mListener != null) { - mListener.onChannelRecordChanged(); + synchronized (sListenerLock) { + if (mListener != null) { + mListener.onChannelRecordChanged(); + } } } }); } } + + private static class RecommendationHandler extends WeakHandler { + public RecommendationHandler(@NonNull Looper looper, RecommendationDataManager ref) { + super(looper, ref); + } + + @Override + public void handleMessage(Message msg, @NonNull RecommendationDataManager dataManager) { + switch (msg.what) { + case MSG_START: + dataManager.onStart(); + break; + case MSG_STOP: + if (dataManager.mStarted) { + dataManager.onStop(); + } + break; + case MSG_UPDATE_CHANNEL: + if (dataManager.mStarted) { + dataManager.onUpdateChannel((Uri) msg.obj); + } + break; + case MSG_UPDATE_CHANNELS: + if (dataManager.mStarted) { + dataManager.onUpdateChannels((Uri) msg.obj); + } + break; + case MSG_UPDATE_WATCH_HISTORY: + if (dataManager.mStarted) { + dataManager.onLoadWatchHistory((Uri) msg.obj); + } + break; + case MSG_NOTIFY_CHANNEL_RECORD_MAP_LOADED: + if (dataManager.mStarted) { + dataManager.onNotifyChannelRecordMapLoaded(); + } + break; + case MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED: + if (dataManager.mStarted) { + dataManager.onNotifyChannelRecordMapChanged(); + } + break; + } + } + } } diff --git a/src/com/android/tv/recommendation/RoutineWatchEvaluator.java b/src/com/android/tv/recommendation/RoutineWatchEvaluator.java index 8f6f203d..694da6bf 100644 --- a/src/com/android/tv/recommendation/RoutineWatchEvaluator.java +++ b/src/com/android/tv/recommendation/RoutineWatchEvaluator.java @@ -17,7 +17,6 @@ package com.android.tv.recommendation; import android.support.annotation.VisibleForTesting; -import android.text.TextUtils; import com.android.tv.data.Program; @@ -26,7 +25,6 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; public class RoutineWatchEvaluator extends Recommender.Evaluator { // TODO: test and refine constant values in WatchedProgramRecommender in order to diff --git a/src/com/android/tv/search/TvProviderSearch.java b/src/com/android/tv/search/TvProviderSearch.java index 2548d34a..00eb68bb 100644 --- a/src/com/android/tv/search/TvProviderSearch.java +++ b/src/com/android/tv/search/TvProviderSearch.java @@ -28,6 +28,7 @@ import android.media.tv.TvContract.WatchedPrograms; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.net.Uri; +import android.support.annotation.WorkerThread; import android.text.TextUtils; import android.util.Log; @@ -76,6 +77,7 @@ public class TvProviderSearch { * @param action One of {@link #ACTION_TYPE_SWITCH_CHANNEL}, {@link #ACTION_TYPE_SWITCH_INPUT}, * or {@link #ACTION_TYPE_AMBIGUOUS}, */ + @WorkerThread public List search(String query, int limit, int action) { List results = new ArrayList<>(); Set channelsFound = new HashSet<>(); @@ -108,8 +110,8 @@ public class TvProviderSearch { return results; } - private StringBuilder appendSelectionString(StringBuilder sb, - String[] columnForExactMatching, String[] columnForPartialMatching) { + private void appendSelectionString(StringBuilder sb, String[] columnForExactMatching, + String[] columnForPartialMatching) { boolean firstColumn = true; if (columnForExactMatching != null) { for (String column : columnForExactMatching) { @@ -131,7 +133,6 @@ public class TvProviderSearch { sb.append(column).append(" LIKE ?"); } } - return sb; } private void insertSelectionArgumentStrings(String[] selectionArgs, int pos, @@ -151,6 +152,7 @@ public class TvProviderSearch { } } + @WorkerThread private List searchChannels(String query, Set channels, int limit) { List results = new ArrayList<>(); if (TextUtils.isDigitsOnly(query)) { @@ -174,6 +176,7 @@ public class TvProviderSearch { return results; } + @WorkerThread private List searchChannels(String query, String[] columnForExactMatching, String[] columnForPartialMatching, Set channelsFound, int limit) { Assert.assertTrue( @@ -246,6 +249,7 @@ public class TvProviderSearch { * program information of the channel if the current program information exists and it is not * blocked. */ + @WorkerThread private void fillProgramInfo(SearchResult result) { long now = System.currentTimeMillis(); Uri uri = TvContract.buildProgramsUriForChannel(result.channelId, now, now); @@ -293,6 +297,7 @@ public class TvProviderSearch { return (int)(100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis)); } + @WorkerThread private List searchPrograms(String query, String[] columnForExactMatching, String[] columnForPartialMatching, Set channelsFound, int limit) { Assert.assertTrue( @@ -459,6 +464,8 @@ public class TvProviderSearch { return result; } + + @WorkerThread private class ChannelComparatorWithSameDisplayNumber implements Comparator { private final Map mMaxWatchStartTimeMap = new HashMap<>(); diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java index 277ccb0a..683e6c3a 100644 --- a/src/com/android/tv/ui/ChannelBannerView.java +++ b/src/com/android/tv/ui/ChannelBannerView.java @@ -124,7 +124,11 @@ public class ChannelBannerView extends FrameLayout implements Channel.LoadImageC @Override public void run() { mCurrentHeight = 0; - mMainActivity.goToEmptyScene(true); + mMainActivity.getOverlayManager().hideOverlays( + TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU); } }; private final long mShowDurationMillis; diff --git a/src/com/android/tv/ui/FullscreenDialogView.java b/src/com/android/tv/ui/FullscreenDialogView.java index 9cda1f3a..e2220722 100644 --- a/src/com/android/tv/ui/FullscreenDialogView.java +++ b/src/com/android/tv/ui/FullscreenDialogView.java @@ -16,13 +16,14 @@ package com.android.tv.ui; -import android.animation.Animator; import android.animation.TimeInterpolator; import android.app.Dialog; import android.content.Context; import android.os.Handler; import android.util.AttributeSet; +import android.util.Log; import android.view.View; +import android.view.ViewTreeObserver; import android.view.animation.AnimationUtils; import android.widget.FrameLayout; @@ -32,18 +33,18 @@ import com.android.tv.dialog.FullscreenDialogFragment; public class FullscreenDialogView extends FrameLayout implements FullscreenDialogFragment.DialogView { + private static final String TAG = "FullscreenDialogView"; + private static final boolean DEBUG = false; + private static final int FADE_IN_DURATION_MS = 400; - private static final int FADE_OUT_DURATION_MS = 300; + private static final int FADE_OUT_DURATION_MS = 250; private static final int TRANSITION_INTERVAL_MS = 300; private MainActivity mActivity; private Dialog mDialog; private boolean mSkipEnterAlphaAnimation; private boolean mSkipExitAlphaAnimation; - private boolean mUseTranslationAnimation; - private final int mEnterTranslationX; - private final int mExitTranslationX; private final TimeInterpolator mLinearOutSlowIn; private final TimeInterpolator mFastOutLinearIn; @@ -57,18 +58,18 @@ public class FullscreenDialogView extends FrameLayout public FullscreenDialogView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mEnterTranslationX = context.getResources().getInteger( - R.integer.fullscreen_dialog_enter_translation_x); - mExitTranslationX = context.getResources().getInteger( - R.integer.fullscreen_dialog_exit_translation_x); mLinearOutSlowIn = AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); mFastOutLinearIn = AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_linear_in); - } - - public void setTransitionAnimationEnabled(boolean enable) { - mUseTranslationAnimation = enable; + getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + getViewTreeObserver().removeOnGlobalLayoutListener(this); + startEnterAnimation(); + } + }); } protected MainActivity getActivity() { @@ -106,12 +107,6 @@ public class FullscreenDialogView extends FrameLayout @Override public void onDestroy() { } - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - startEnterAnimation(); - } - /** * Transitions to another view inside the host {@link Dialog}. */ @@ -133,70 +128,45 @@ public class FullscreenDialogView extends FrameLayout }); } + /** + * Called when an enter animation starts. Sub-view specific animation can be implemented. + */ + protected void onStartEnterAnimation(TimeInterpolator interpolator, long duration) { + } + + /** + * Called when an exit animation starts. Sub-view specific animation can be implemented. + */ + protected void onStartExitAnimation(TimeInterpolator interpolator, long duration, + Runnable onAnimationEnded) { + } + private void startEnterAnimation() { - View v = findViewById(R.id.container); - if (mUseTranslationAnimation) { - v.setTranslationX(mEnterTranslationX); - v.animate() - .translationX(0) - .setInterpolator(mLinearOutSlowIn) - .setDuration(FADE_IN_DURATION_MS) - .setListener(new HardwareLayerAnimatorListenerAdapter(this)) - .start(); - } + if (DEBUG) Log.d(TAG, "start an enter animation"); + View backgroundView = findViewById(R.id.background); if (!mSkipEnterAlphaAnimation) { - setAlpha(0); - animate() - .alpha(1.0f) - .setInterpolator(mLinearOutSlowIn) - .setDuration(FADE_IN_DURATION_MS) - .setListener(new HardwareLayerAnimatorListenerAdapter(this)) - .start(); - } else { - v.setAlpha(0); - v.animate() + backgroundView.setAlpha(0); + backgroundView.animate() .alpha(1.0f) .setInterpolator(mLinearOutSlowIn) .setDuration(FADE_IN_DURATION_MS) - .setListener(new HardwareLayerAnimatorListenerAdapter(this)) + .withLayer() .start(); } + onStartEnterAnimation(mLinearOutSlowIn, FADE_IN_DURATION_MS); } private void startExitAnimation(final Runnable onAnimationEnded) { - View v = findViewById(R.id.container); - if (mUseTranslationAnimation) { - v.animate() - .translationX(mExitTranslationX) - .setInterpolator(mFastOutLinearIn) - .setDuration(FADE_OUT_DURATION_MS) - .setListener(new HardwareLayerAnimatorListenerAdapter(this)) - .start(); - } + if (DEBUG) Log.d(TAG, "start an exit animation"); + View backgroundView = findViewById(R.id.background); if (!mSkipExitAlphaAnimation) { - animate() - .alpha(0.0f) - .setInterpolator(mFastOutLinearIn) - .setDuration(FADE_OUT_DURATION_MS) - .setListener(new HardwareLayerAnimatorListenerAdapter(this) { - @Override - public void onAnimationEnd(Animator animation) { - onAnimationEnded.run(); - } - }) - .start(); - } else { - v.animate() + backgroundView.animate() .alpha(0.0f) .setInterpolator(mFastOutLinearIn) .setDuration(FADE_OUT_DURATION_MS) - .setListener(new HardwareLayerAnimatorListenerAdapter(this) { - @Override - public void onAnimationEnd(Animator animation) { - onAnimationEnded.run(); - } - }) + .withLayer() .start(); } + onStartExitAnimation(mFastOutLinearIn, FADE_OUT_DURATION_MS, onAnimationEnded); } } diff --git a/src/com/android/tv/ui/InputBannerView.java b/src/com/android/tv/ui/InputBannerView.java index cc7a9443..649331f4 100644 --- a/src/com/android/tv/ui/InputBannerView.java +++ b/src/com/android/tv/ui/InputBannerView.java @@ -34,7 +34,11 @@ public class InputBannerView extends LinearLayout implements TvTransitionManager private final Runnable mHideRunnable = new Runnable() { @Override public void run() { - ((MainActivity) getContext()).goToEmptyScene(true); + ((MainActivity) getContext()).getOverlayManager().hideOverlays( + TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU); } }; diff --git a/src/com/android/tv/ui/IntroView.java b/src/com/android/tv/ui/IntroView.java index 4266b30d..7530f283 100644 --- a/src/com/android/tv/ui/IntroView.java +++ b/src/com/android/tv/ui/IntroView.java @@ -16,6 +16,7 @@ package com.android.tv.ui; +import android.animation.TimeInterpolator; import android.content.Context; import android.graphics.drawable.AnimationDrawable; import android.util.AttributeSet; @@ -23,7 +24,7 @@ import android.view.KeyEvent; import android.view.View; import com.android.tv.R; -import com.android.tv.menu.MenuView; +import com.android.tv.menu.Menu; public class IntroView extends FullscreenDialogView { private AnimationDrawable mRippleDrawable; @@ -39,7 +40,6 @@ public class IntroView extends FullscreenDialogView { public IntroView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - setTransitionAnimationEnabled(false); } @Override @@ -78,7 +78,37 @@ public class IntroView extends FullscreenDialogView { @Override public void onDestroy() { if (mOpenMenu) { - getActivity().getOverlayManager().showMenu(MenuView.REASON_GUIDE); + getActivity().getOverlayManager().showMenu(Menu.REASON_GUIDE); } } + + @Override + protected void onStartEnterAnimation(TimeInterpolator interpolator, long duration) { + View v = findViewById(R.id.container); + v.setAlpha(0); + v.animate() + .alpha(1.0f) + .setInterpolator(interpolator) + .setDuration(duration) + .withLayer() + .start(); + } + + @Override + protected void onStartExitAnimation(TimeInterpolator interpolator, long duration, + final Runnable onAnimationEnded) { + View v = findViewById(R.id.container); + v.animate() + .alpha(0.0f) + .setInterpolator(interpolator) + .setDuration(duration) + .withLayer() + .withEndAction(new Runnable() { + @Override + public void run() { + onAnimationEnded.run(); + } + }) + .start(); + } } diff --git a/src/com/android/tv/ui/KeypadChannelSwitchView.java b/src/com/android/tv/ui/KeypadChannelSwitchView.java index bfbb6e9c..3ba2738e 100644 --- a/src/com/android/tv/ui/KeypadChannelSwitchView.java +++ b/src/com/android/tv/ui/KeypadChannelSwitchView.java @@ -79,7 +79,11 @@ public class KeypadChannelSwitchView extends LinearLayout implements mMainActivity.tuneToChannel(mSelectedChannel); mTracker.sendChannelNumberItemChosenByTimeout(); } else { - mMainActivity.goToEmptyScene(true); + mMainActivity.getOverlayManager().hideOverlays( + TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU); } } }; diff --git a/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java b/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java new file mode 100644 index 00000000..2a59e6f6 --- /dev/null +++ b/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.tv.ui; + +import android.os.Message; +import android.support.annotation.NonNull; +import android.support.v17.leanback.widget.VerticalGridView; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; + +import com.android.tv.common.WeakHandler; + +/** + * Listener to make focus change faster over time. + */ +public class OnRepeatedKeyInterceptListener implements VerticalGridView.OnKeyInterceptListener { + private static final String TAG = "OnRepeatedKeyInterceptListener"; + private static final boolean DEBUG = false; + + private static final int[] THRESHOLD_FAST_FOCUS_CHANGE_TIME_MS = { 2000, 5000 }; + private static final int[] MAX_SKIPPED_VIEW_COUNT = { 1, 4 }; + private static final int MSG_MOVE_FOCUS = 1000; + + private VerticalGridView mView; + private MyHandler mHandler = new MyHandler(this); + private int mDirection; + private boolean mFocusAccelerated; + private long mRepeatedKeyInterval; + + public OnRepeatedKeyInterceptListener(VerticalGridView view) { + mView = view; + } + + public boolean isFocusAccelerated() { + return mFocusAccelerated; + } + + @Override + public boolean onInterceptKeyEvent(KeyEvent event) { + mHandler.removeMessages(MSG_MOVE_FOCUS); + if (event.getKeyCode() != KeyEvent.KEYCODE_DPAD_UP && + event.getKeyCode() != KeyEvent.KEYCODE_DPAD_DOWN) { + return false; + } + + long duration = event.getEventTime() - event.getDownTime(); + if (duration < THRESHOLD_FAST_FOCUS_CHANGE_TIME_MS[0] + || event.isCanceled()) { + mFocusAccelerated = false; + return false; + } + mDirection = event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP ? View.FOCUS_UP + : View.FOCUS_DOWN; + int skippedViewCount = MAX_SKIPPED_VIEW_COUNT[0]; + for (int i = 1 ;i < THRESHOLD_FAST_FOCUS_CHANGE_TIME_MS.length; ++i) { + if (THRESHOLD_FAST_FOCUS_CHANGE_TIME_MS[i] < duration) { + skippedViewCount = MAX_SKIPPED_VIEW_COUNT[i]; + } else { + break; + } + } + if (event.getAction() == KeyEvent.ACTION_DOWN) { + mRepeatedKeyInterval = duration / event.getRepeatCount(); + mFocusAccelerated = true; + } else { + // HACK: we move focus skippedViewCount times more even after ACTION_UP. Without this + // hack, a focused view's position doesn't reach to the desired position + // in ProgramGrid. + mFocusAccelerated = false; + } + for (int i = 0; i < skippedViewCount; ++i) { + mHandler.sendEmptyMessageDelayed(MSG_MOVE_FOCUS, + mRepeatedKeyInterval * i / (skippedViewCount + 1)); + } + if (DEBUG) Log.d(TAG, "onInterceptKeyEvent: focused view " + mView.findFocus()); + return false; + } + + private static class MyHandler extends WeakHandler { + private MyHandler(OnRepeatedKeyInterceptListener listener) { + super(listener); + } + + @Override + public void handleMessage(Message msg, @NonNull OnRepeatedKeyInterceptListener listener) { + if (msg.what == MSG_MOVE_FOCUS) { + View focused = listener.mView.findFocus(); + if (DEBUG) Log.d(TAG, "MSG_MOVE_FOCUS: focused view " + focused); + if (focused != null) { + View v = focused.focusSearch(listener.mDirection); + if (v != null && v != focused) { + v.requestFocus(listener.mDirection); + } + } + } + } + } +} diff --git a/src/com/android/tv/ui/SelectInputView.java b/src/com/android/tv/ui/SelectInputView.java index 54889d8d..e347fbb1 100644 --- a/src/com/android/tv/ui/SelectInputView.java +++ b/src/com/android/tv/ui/SelectInputView.java @@ -68,7 +68,11 @@ public class SelectInputView extends VerticalGridView implements if (mSelectedInput == null || TextUtils.equals(mSelectedInput.getId(), mCurrentInputId) || (!mSelectedInput.isPassthroughInput() && mCurrentInputId == null)) { - mMainActivity.goToEmptyScene(true); + mMainActivity.getOverlayManager().hideOverlays( + TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU); return; } // TODO: pass english label to tracker http://b/22355024 diff --git a/src/com/android/tv/ui/SetupView.java b/src/com/android/tv/ui/SetupView.java index ba90dcfe..330b7e9f 100644 --- a/src/com/android/tv/ui/SetupView.java +++ b/src/com/android/tv/ui/SetupView.java @@ -16,14 +16,21 @@ package com.android.tv.ui; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.TimeInterpolator; import android.app.Dialog; import android.content.Context; -import android.content.pm.ApplicationInfo; import android.media.tv.TvInputInfo; +import android.media.tv.TvInputManager.TvInputCallback; import android.support.annotation.VisibleForTesting; import android.support.v17.leanback.widget.VerticalGridView; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; +import android.util.Log; import android.util.TypedValue; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -38,17 +45,22 @@ import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; public class SetupView extends FullscreenDialogView { + private static final String TAG = "SetupView"; + private static final boolean DEBUG = false; + private static final int FINISH_ACTIVITY_DELAY_MS = 200; private static final int REFRESH_DELAY_MS_AFTER_WINDOW_FOCUS_GAINED = 200; + private static final long ANIMATION_START_DELAY = 25; + private VerticalGridView mInputView; private ChannelDataManager mChannelDataManager; + private TvInputManagerHelper mInputManager; private List mInputList; // mInputList[0:mKnownInputStartIndex - 1] are new inputs. // And mInputList[mKnownInputStartIndex:end] are inputs which have been shown in SetupView. @@ -59,25 +71,52 @@ public class SetupView extends FullscreenDialogView { private boolean mInitialized; private SetupUtils mSetupUtils; private boolean mNeedIntroDialog; + private final int mEnterTranslationX; + private final int mExitTranslationX; + private Animator mEnterAnimator; - private final ChannelDataManager.Listener mChannelDataListener = new ChannelDataManager.Listener() { - @Override - public void onLoadFinished() { } - + private final TvInputCallback mInputCallback = new TvInputCallback() { @Override - public void onChannelListUpdated() { - if (mAdapter != null) { - mAdapter.notifyDataSetChanged(); + public void onInputAdded(String inputId) { + if (DEBUG) { + Log.d(TAG, "onInputAdded: " + inputId); + } + if (!mInitialized) { + return; } + updateInputList(); } @Override - public void onChannelBrowsableChanged() { - if (mAdapter != null) { - mAdapter.notifyDataSetChanged(); + public void onInputRemoved(String inputId) { + if (DEBUG) { + Log.d(TAG, "onInputRemoved: " + inputId); + } + if (!mInitialized) { + return; } + updateInputList(); } }; + private final ChannelDataManager.Listener mChannelDataListener = + new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { } + + @Override + public void onChannelListUpdated() { + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + } + + @Override + public void onChannelBrowsableChanged() { + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + } + }; public SetupView(Context context) { this(context, null, 0); @@ -89,7 +128,10 @@ public class SetupView extends FullscreenDialogView { public SetupView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - setTransitionAnimationEnabled(true); + mEnterTranslationX = context.getResources().getInteger( + R.integer.fullscreen_dialog_enter_translation_x); + mExitTranslationX = context.getResources().getInteger( + R.integer.fullscreen_dialog_exit_translation_x); } @Override @@ -105,6 +147,18 @@ public class SetupView extends FullscreenDialogView { mInputView.setWindowAlignmentOffsetPercent(outValue.getFloat()); } + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + mInputManager.addCallback(mInputCallback); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mInputManager.removeCallback(mInputCallback); + } + @Override public boolean dispatchKeyEvent(KeyEvent event) { return mClosing || super.dispatchKeyEvent(event); @@ -138,12 +192,21 @@ public class SetupView extends FullscreenDialogView { throw new IllegalStateException("initialize() is called more than once"); } mInitialized = true; - final TvInputManagerHelper inputManager = getActivity().getTvInputManagerHelper(); + mInputManager = getActivity().getTvInputManagerHelper(); mChannelDataManager = getActivity().getChannelDataManager(); mSetupUtils = SetupUtils.getInstance(activity); + mNeedIntroDialog = mSetupUtils.isFirstTune(); + mAdapter = new SetupAdapter(); + mInputView.setAdapter(mAdapter); + mChannelDataManager.addListener(mChannelDataListener); + updateInputList(); + } + + private void updateInputList() { + mInputList = new ArrayList<>(); mKnownInputStartIndex = 0; - mInputList = inputManager.getTvInputInfos(true, true); - Collections.sort(mInputList, new TvInputInfoComparator(mSetupUtils, inputManager)); + mInputList = mInputManager.getTvInputInfos(true, true); + Collections.sort(mInputList, new TvInputInfoComparator(mSetupUtils, mInputManager)); for (TvInputInfo input : mInputList) { if (mSetupUtils.isNewInput(input.getId())) { mSetupUtils.markAsKnownInput(input.getId()); @@ -151,10 +214,8 @@ public class SetupView extends FullscreenDialogView { } } mShowDivider = mKnownInputStartIndex != 0 && mKnownInputStartIndex != mInputList.size(); - mAdapter = new SetupAdapter(); - mInputView.setAdapter(mAdapter); - mChannelDataManager.addListener(mChannelDataListener); mNeedIntroDialog = mSetupUtils.isFirstTune(); + mAdapter.notifyDataSetChanged(); } /** @@ -188,6 +249,79 @@ public class SetupView extends FullscreenDialogView { dismiss(); } + @Override + protected void onStartEnterAnimation(final TimeInterpolator interpolator, final long duration) { + List animatorList = new ArrayList<>(); + View leftPanel = findViewById(R.id.setup_left); + leftPanel.setAlpha(0); + leftPanel.setTranslationX(mEnterTranslationX); + animatorList.add(buildEnterAnimator(leftPanel, duration, 0, interpolator)); + + for (int i = 0; i < mInputView.getChildCount(); ++i) { + View itemView = mInputView.getChildAt(i); + itemView.setAlpha(0); + itemView.setTranslationX(mEnterTranslationX); + int itemPosition = mInputView.getChildAdapterPosition(itemView); + animatorList.add(buildEnterAnimator(itemView, duration, + ANIMATION_START_DELAY * (itemPosition + 1), interpolator)); + } + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(animatorList); + mEnterAnimator = animatorSet; + mEnterAnimator.start(); + } + + private Animator buildEnterAnimator(View v, long duration, long startDelay, + TimeInterpolator interpolator) { + Animator animator = ObjectAnimator.ofPropertyValuesHolder(v, + PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1.0f), + PropertyValuesHolder.ofFloat(View.TRANSLATION_X, mEnterTranslationX, 0)); + animator.setStartDelay(startDelay); + animator.setDuration(duration); + animator.setInterpolator(interpolator); + animator.addListener(new HardwareLayerAnimatorListenerAdapter(v)); + return animator; + } + + @Override + protected void onStartExitAnimation(TimeInterpolator interpolator, long duration, + final Runnable onAnimationEnded) { + if (mEnterAnimator != null && mEnterAnimator.isRunning()) { + mEnterAnimator.cancel(); + } + List animatorList = new ArrayList<>(); + animatorList.add( + buildExitAnimator(findViewById(R.id.setup_left), duration, 0, interpolator)); + for (int i = 0; i < mInputView.getChildCount(); ++i) { + View itemView = mInputView.getChildAt(i); + int itemPosition = mInputView.getChildAdapterPosition(itemView); + animatorList.add(buildExitAnimator(itemView, duration, + ANIMATION_START_DELAY * (itemPosition + 1), interpolator)); + } + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(animatorList); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + onAnimationEnded.run(); + } + }); + animatorSet.start(); + } + + private Animator buildExitAnimator(View v, long duration, long startDelay, + TimeInterpolator interpolator) { + Animator animator = ObjectAnimator.ofPropertyValuesHolder(v, + PropertyValuesHolder.ofFloat(View.ALPHA, v.getAlpha(), 0f), + PropertyValuesHolder.ofFloat(View.TRANSLATION_X, + v.getTranslationX(), mExitTranslationX)); + animator.setStartDelay(startDelay); + animator.setDuration(duration); + animator.setInterpolator(interpolator); + animator.addListener(new HardwareLayerAnimatorListenerAdapter(v)); + return animator; + } + private class SetupAdapter extends RecyclerView.Adapter { @Override public int getItemViewType(int position) { @@ -286,8 +420,8 @@ public class SetupView extends FullscreenDialogView { @VisibleForTesting static class TvInputInfoComparator implements Comparator { - private SetupUtils mSetupUtils; - private TvInputManagerHelper mInputManager; + private final SetupUtils mSetupUtils; + private final TvInputManagerHelper mInputManager; public TvInputInfoComparator(SetupUtils setupUtils, TvInputManagerHelper inputManager) { mSetupUtils = setupUtils; @@ -303,5 +437,5 @@ public class SetupView extends FullscreenDialogView { } return mInputManager.getDefaultTvInputInfoComparator().compare(lhs, rhs); } - }; + } } diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java index cbf61304..eba43594 100644 --- a/src/com/android/tv/ui/TunableTvView.java +++ b/src/com/android/tv/ui/TunableTvView.java @@ -53,6 +53,7 @@ import com.android.tv.common.TvCommonConstants; import com.android.tv.data.Channel; import com.android.tv.data.StreamInfo; import com.android.tv.parental.ContentRatingsManager; +import com.android.tv.recommendation.NotificationService; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -233,7 +234,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { List tracks = getTracks(type); boolean trackFound = false; if (tracks != null) { - for (TvTrackInfo track : getTracks(type)) { + for (TvTrackInfo track : tracks) { if (track.getId().equals(trackId)) { if (type == TvTrackInfo.TYPE_VIDEO) { mVideoWidth = track.getVideoWidth(); @@ -454,7 +455,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } mOnTuneListener = listener; mCurrentChannel = channel; - mTracker.sendChannelViewStart(mCurrentChannel); + boolean tunedByRecommendation = params != null + && params.getString(NotificationService.TUNE_PARAMS_RECOMMENDATION_TYPE) != null; + mTracker.sendChannelViewStart(mCurrentChannel, tunedByRecommendation); mChannelViewTimer.start(); boolean needSurfaceSizeUpdate = false; if (!inputInfo.equals(mInputInfo)) { diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java index 28f16980..b3c009d8 100644 --- a/src/com/android/tv/ui/TvOverlayManager.java +++ b/src/com/android/tv/ui/TvOverlayManager.java @@ -16,9 +16,13 @@ package com.android.tv.ui; +import android.os.Handler; +import android.os.Message; import android.support.annotation.IntDef; +import android.support.annotation.NonNull; import android.util.Log; import android.view.KeyEvent; +import android.view.ViewGroup; import com.android.tv.ChannelTuner; import com.android.tv.MainActivity; @@ -27,13 +31,16 @@ import com.android.tv.R; import com.android.tv.TimeShiftManager; import com.android.tv.TvApplication; import com.android.tv.analytics.Tracker; +import com.android.tv.common.WeakHandler; import com.android.tv.dialog.FullscreenDialogFragment; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dialog.RecentlyWatchedDialogFragment; import com.android.tv.dialog.SafeDismissDialogFragment; import com.android.tv.guide.ProgramGuide; +import com.android.tv.menu.Menu; +import com.android.tv.menu.Menu.MenuShowReason; +import com.android.tv.menu.MenuRowFactory; import com.android.tv.menu.MenuView; -import com.android.tv.menu.MenuView.MenuShowReason; import com.android.tv.search.ProgramGuideSearchFragment; import com.android.tv.ui.TvTransitionManager.SceneType; import com.android.tv.ui.sidepanel.AboutFragment; @@ -60,7 +67,7 @@ public class TvOverlayManager { value = {FLAG_HIDE_OVERLAYS_DEFAULT, FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION, FLAG_HIDE_OVERLAYS_KEEP_SCENE, FLAG_HIDE_OVERLAYS_KEEP_DIALOG, FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS, FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY, - FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE}) + FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE, FLAG_HIDE_OVERLAYS_KEEP_MENU}) public @interface HideOverlayFlag {} // FLAG_HIDE_OVERLAYs must be bitwise exclusive. public static final int FLAG_HIDE_OVERLAYS_DEFAULT = 0b00000000; @@ -70,6 +77,9 @@ public class TvOverlayManager { public static final int FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS = 0b00010000; public static final int FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY = 0b00100000; public static final int FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE = 0b01000000; + public static final int FLAG_HIDE_OVERLAYS_KEEP_MENU = 0b10000000; + + public static final int MSG_SHOW_DIALOG = 1000; @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, @@ -101,7 +111,7 @@ public class TvOverlayManager { private final MainActivity mMainActivity; private final ChannelTuner mChannelTuner; private final TvTransitionManager mTransitionManager; - private final MenuView mMenuView; + private final Menu mMenu; private final SideFragmentManager mSideFragmentManager; private final ProgramGuide mProgramGuide; private final KeypadChannelSwitchView mKeypadChannelSwitchView; @@ -109,20 +119,24 @@ public class TvOverlayManager { private final ProgramGuideSearchFragment mSearchFragment; private final Tracker mTracker; private SafeDismissDialogFragment mCurrentDialog; + private final Handler mHandler = new TvOverlayHandler(this); private @TvOverlayType int mOpenedOverlays; public TvOverlayManager(MainActivity mainActivity, ChannelTuner channelTuner, - TvTransitionManager transitionManager, KeypadChannelSwitchView keypadChannelSwitchView, - SelectInputView selectInputView, ProgramGuideSearchFragment searchFragment) { + KeypadChannelSwitchView keypadChannelSwitchView, + ChannelBannerView channelBannerView, InputBannerView inputBannerView, + SelectInputView selectInputView, ViewGroup sceneContainer, + ProgramGuideSearchFragment searchFragment) { mMainActivity = mainActivity; mChannelTuner = channelTuner; - mTransitionManager = transitionManager; mKeypadChannelSwitchView = keypadChannelSwitchView; mSelectInputView = selectInputView; mSearchFragment = searchFragment; mTracker = ((TvApplication) mainActivity.getApplication()).getTracker(); - transitionManager.setListener(new TvTransitionManager.Listener() { + mTransitionManager = new TvTransitionManager(mainActivity, sceneContainer, + channelBannerView, inputBannerView, mKeypadChannelSwitchView, selectInputView); + mTransitionManager.setListener(new TvTransitionManager.Listener() { @Override public void onSceneChanged(int fromScene, int toScene) { // Call notifyOverlayOpened first so that the listener can know that a new scene @@ -136,19 +150,18 @@ public class TvOverlayManager { } }); // Menu - mMenuView = (MenuView) mainActivity.findViewById(R.id.menu); - mMenuView.setPreShowCallback(new Runnable() { - @Override - public void run() { - onOverlayOpened(OVERLAY_TYPE_MENU); - } - }); - mMenuView.setPostHideCallback(new Runnable() { - @Override - public void run() { - onOverlayClosed(OVERLAY_TYPE_MENU); - } - }); + MenuView menuView = (MenuView) mainActivity.findViewById(R.id.menu); + mMenu = new Menu(mainActivity, menuView, new MenuRowFactory(mainActivity), + new Menu.OnMenuVisibilityChangeListener() { + @Override + public void onMenuVisibilityChange(boolean visible) { + if (visible) { + onOverlayOpened(OVERLAY_TYPE_MENU); + } else { + onOverlayClosed(OVERLAY_TYPE_MENU); + } + } + }); // Side Fragment mSideFragmentManager = new SideFragmentManager(mainActivity, new Runnable() { @@ -185,26 +198,19 @@ public class TvOverlayManager { } /** - * A method to follow the lifecycle of the {@link MainActivity}. - * This is called from {@link MainActivity#onStart}. + * A method to release all the allocated resources or unregister listeners. + * This is called from {@link MainActivity#onDestroy}. */ - public void onStart() { - mMenuView.onStart(); + public void release() { + mMenu.release(); + mHandler.removeCallbacksAndMessages(null); } /** - * A method to follow the lifecycle of the {@link MainActivity}. - * This is called from {@link MainActivity#onStop}. + * Returns the instance of {@link Menu}. */ - public void onStop() { - mMenuView.onStop(); - } - - /** - * Returns the instance of {@link MenuView}. - */ - public MenuView getMenuView() { - return mMenuView; + public Menu getMenu() { + return mMenu; } /** @@ -233,7 +239,7 @@ public class TvOverlayManager { */ public void showMenu(@MenuShowReason int reason) { if (mChannelTuner != null && mChannelTuner.areAllChannelsLoaded()) { - mMenuView.show(reason); + mMenu.show(reason); } } @@ -242,7 +248,7 @@ public class TvOverlayManager { */ public boolean showMenuWithTimeShiftPauseIfNeeded() { if (mMainActivity.getTimeShiftManager().isPaused()) { - showMenu(MenuView.REASON_PLAY_CONTROLS_PAUSE); + showMenu(Menu.REASON_PLAY_CONTROLS_PAUSE); return true; } return false; @@ -253,6 +259,14 @@ public class TvOverlayManager { */ public void showDialogFragment(String tag, SafeDismissDialogFragment dialog, boolean keepSidePanelHistory) { + showDialogFragment(tag, dialog, keepSidePanelHistory, 0); + } + + /** + * Shows the given dialog with a delay {@code delayMillis}. + */ + public void showDialogFragment(String tag, SafeDismissDialogFragment dialog, + boolean keepSidePanelHistory, long delayMillis) { int flags = FLAG_HIDE_OVERLAYS_KEEP_DIALOG; if (keepSidePanelHistory) { flags |= FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY; @@ -269,7 +283,11 @@ public class TvOverlayManager { } mCurrentDialog = dialog; - dialog.show(mMainActivity.getFragmentManager(), tag); + if (delayMillis == 0) { + dialog.show(mMainActivity.getFragmentManager(), tag); + } else { + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SHOW_DIALOG, tag), delayMillis); + } // Calling this from SafeDismissDialogFragment.onCreated() might be late // because it takes time for onCreated to be called @@ -281,9 +299,17 @@ public class TvOverlayManager { * Shows setup dialog. */ public void showSetupDialog() { + showSetupDialog(0); + } + + /** + * Shows setup dialog with a delay {@code delayMillis}. + */ + public void showSetupDialog(long delayMillis) { if (DEBUG) Log.d(TAG,"showSetupDialog"); showDialogFragment(FullscreenDialogFragment.DIALOG_TAG, - new FullscreenDialogFragment(R.layout.setup_dialog, SETUP_TRACKER_LABEL), false); + new FullscreenDialogFragment(R.layout.setup_dialog, SETUP_TRACKER_LABEL), false, + delayMillis); } /** @@ -303,6 +329,35 @@ public class TvOverlayManager { new RecentlyWatchedDialogFragment(), false); } + /** + * Shows banner view. + */ + public void showBanner() { + mTransitionManager.goToChannelBannerScene(); + } + + public void showKeypadChannelSwitch() { + hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG); + mTransitionManager.goToKeypadChannelSwitchScene(); + } + + /** + * Shows select input view. + */ + public void showSelectInputView() { + hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE); + mTransitionManager.goToSelectInputScene(); + } + + /** + * Initializes animators if animators are not initialized yet. + */ + public void initAnimatorIfNeeded() { + mTransitionManager.initIfNeeded(); + } + /** * It is called when a SafeDismissDialogFragment is destroyed. */ @@ -315,8 +370,12 @@ public class TvOverlayManager { * Shows the program guide. */ public void showProgramGuide() { - hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE); - mProgramGuide.show(); + mProgramGuide.show(new Runnable() { + @Override + public void run() { + hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE); + } + }); } /** @@ -337,14 +396,23 @@ public class TvOverlayManager { // to null. ((PinDialogFragment) mCurrentDialog).setResultListener(null); } - mCurrentDialog.dismiss(); + if (mHandler.hasMessages(MSG_SHOW_DIALOG)) { + mHandler.removeMessages(MSG_SHOW_DIALOG); + onDialogDestroyed(); + } else { + mCurrentDialog.dismiss(); + } } mCurrentDialog = null; } boolean withAnimation = (flags & FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION) == 0; - mMenuView.hide(withAnimation); + if ((flags & FLAG_HIDE_OVERLAYS_KEEP_MENU) != 0) { + // Keeps the menu. + } else { + mMenu.hide(withAnimation); + } if ((flags & FLAG_HIDE_OVERLAYS_KEEP_SCENE) != 0) { // Keeps the current scene. } else { @@ -366,6 +434,17 @@ public class TvOverlayManager { } } + /** + * Returns true, if a main view needs to hide informational text. Specifically, when overlay + * UIs except banner is shown, the informational text needs to be hidden for clean UI. + */ + public boolean needHideTextOnMainView() { + return getSideFragmentManager().isActive() + || getMenu().isActive() + || mTransitionManager.isKeypadChannelSwitchActive() + || mTransitionManager.isSelectInputActive(); + } + @TvOverlayType private int convertSceneToOverlayType(@SceneType int sceneType) { switch (sceneType) { case TvTransitionManager.SCENE_TYPE_CHANNEL_BANNER: @@ -419,8 +498,8 @@ public class TvOverlayManager { public void onUserInteraction() { if (mSideFragmentManager.isActive()) { mSideFragmentManager.scheduleHideAll(); - } else if (mMenuView.isActive()) { - mMenuView.scheduleHide(); + } else if (mMenu.isActive()) { + mMenu.scheduleHide(); } else if (mProgramGuide.isActive()) { mProgramGuide.scheduleHide(); } @@ -439,7 +518,7 @@ public class TvOverlayManager { // Consumes the keys which may trigger system's default music player. return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } - if (mMenuView.isActive() || mSideFragmentManager.isActive() || mProgramGuide.isActive()) { + if (mMenu.isActive() || mSideFragmentManager.isActive() || mProgramGuide.isActive()) { return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY; } if (mTransitionManager.isKeypadChannelSwitchActive()) { @@ -477,31 +556,31 @@ public class TvOverlayManager { switch (keyCode) { case KeyEvent.KEYCODE_MEDIA_PLAY: timeShiftManager.play(); - showMenu(MenuView.REASON_PLAY_CONTROLS_PLAY); + showMenu(Menu.REASON_PLAY_CONTROLS_PLAY); break; case KeyEvent.KEYCODE_MEDIA_PAUSE: timeShiftManager.pause(); - showMenu(MenuView.REASON_PLAY_CONTROLS_PAUSE); + showMenu(Menu.REASON_PLAY_CONTROLS_PAUSE); break; case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: timeShiftManager.togglePlayPause(); - showMenu(MenuView.REASON_PLAY_CONTROLS_PLAY_PAUSE); + showMenu(Menu.REASON_PLAY_CONTROLS_PLAY_PAUSE); break; case KeyEvent.KEYCODE_MEDIA_REWIND: timeShiftManager.rewind(); - showMenu(MenuView.REASON_PLAY_CONTROLS_REWIND); + showMenu(Menu.REASON_PLAY_CONTROLS_REWIND); break; case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: timeShiftManager.fastForward(); - showMenu(MenuView.REASON_PLAY_CONTROLS_FAST_FORWARD); + showMenu(Menu.REASON_PLAY_CONTROLS_FAST_FORWARD); break; case KeyEvent.KEYCODE_MEDIA_PREVIOUS: timeShiftManager.jumpToPrevious(); - showMenu(MenuView.REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS); + showMenu(Menu.REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS); break; case KeyEvent.KEYCODE_MEDIA_NEXT: timeShiftManager.jumpToNext(); - showMenu(MenuView.REASON_PLAY_CONTROLS_JUMP_TO_NEXT); + showMenu(Menu.REASON_PLAY_CONTROLS_JUMP_TO_NEXT); break; default: // Does nothing. @@ -513,7 +592,7 @@ public class TvOverlayManager { if (mTransitionManager.isSelectInputActive()) { mSelectInputView.onKeyUp(keyCode, event); } else { - mMainActivity.showSelectInputView(); + showSelectInputView(); } return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } @@ -537,7 +616,7 @@ public class TvOverlayManager { } return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY; } - if (mMenuView.isActive() || mTransitionManager.isSceneActive()) { + if (mMenu.isActive() || mTransitionManager.isSceneActive()) { if (keyCode == KeyEvent.KEYCODE_BACK) { TimeShiftManager timeShiftManager = mMainActivity.getTimeShiftManager(); if (timeShiftManager.isPaused()) { @@ -547,7 +626,7 @@ public class TvOverlayManager { | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG); return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } - if (mMenuView.isActive()) { + if (mMenu.isActive()) { if (KeypadChannelSwitchView.isChannelNumberKey(keyCode)) { mMainActivity.showKeypadChannelSwitchView(keyCode); return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; @@ -592,4 +671,19 @@ public class TvOverlayManager { } return false; } + + private static class TvOverlayHandler extends WeakHandler { + public TvOverlayHandler(TvOverlayManager ref) { + super(ref); + } + + @Override + public void handleMessage(Message msg, @NonNull TvOverlayManager tvOverlayManager) { + if (msg.what == MSG_SHOW_DIALOG) { + String tag = (String) msg.obj; + tvOverlayManager.mCurrentDialog + .show(tvOverlayManager.mMainActivity.getFragmentManager(), tag); + } + } + } } diff --git a/src/com/android/tv/ui/TvViewUiManager.java b/src/com/android/tv/ui/TvViewUiManager.java index 3793d245..f93cf45c 100644 --- a/src/com/android/tv/ui/TvViewUiManager.java +++ b/src/com/android/tv/ui/TvViewUiManager.java @@ -99,6 +99,11 @@ public class TvViewUiManager { private MarginLayoutParams mOldTvViewFrame; private ObjectAnimator mBackgroundAnimator; private int mBackgroundColor; + private int mAppliedDisplayedMode = DisplayMode.MODE_NOT_DEFINED; + private int mAppliedVideoWidth; + private int mAppliedVideoHeight; + private int mAppliedTvViewStartMargin; + private int mAppliedTvViewEndMargin; public TvViewUiManager(Context context, TunableTvView tvView, TunableTvView pipView, FrameLayout contentView, TvOptionsManager tvOptionManager) { @@ -442,7 +447,7 @@ public class TvViewUiManager { mBackgroundAnimator.setInterpolator(mFastOutLinearIn); mBackgroundAnimator.start(); } - // In the 'else'case (TV activity is getting out of the shrunken tv view mode and will + // In the 'else' case (TV activity is getting out of the shrunken tv view mode and will // have a pillar box), we keep the background color and don't show the animation. } else { mContentView.setBackgroundColor(color); @@ -698,6 +703,19 @@ public class TvViewUiManager { } private void applyDisplayMode(int videoWidth, int videoHeight, boolean animate) { + if (mAppliedDisplayedMode == mDisplayMode + && mAppliedVideoWidth == videoWidth + && mAppliedVideoHeight == videoHeight + && mAppliedTvViewStartMargin == mTvViewStartMargin + && mAppliedTvViewEndMargin == mTvViewEndMargin) { + return; + } else { + mAppliedDisplayedMode = mDisplayMode; + mAppliedVideoHeight = videoHeight; + mAppliedVideoWidth = videoWidth; + mAppliedTvViewStartMargin = mTvViewStartMargin; + mAppliedTvViewEndMargin = mTvViewEndMargin; + } int availableAreaWidth = mScreenWidth - mTvViewStartMargin - mTvViewEndMargin; int availableAreaHeight = availableAreaWidth * mScreenHeight / mScreenWidth; FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(0, 0, diff --git a/src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java b/src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java index a1d527a5..a95b8149 100644 --- a/src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java +++ b/src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java @@ -28,6 +28,9 @@ import java.util.List; public class ChannelSourcesFragment extends SideFragment { private static final String TRACKER_LABEL = "channel sources"; + + private static int ADDITIONAL_DELAY_TO_SHOW_SETUP_DIALOG_MILLIS = 50; + private final long mCurrentChannelId; public ChannelSourcesFragment(long currentChannelId) { @@ -80,7 +83,11 @@ public class ChannelSourcesFragment extends SideFragment { @Override protected void onSelected() { closeFragment(); - activity.getOverlayManager().showSetupDialog(); + // Running two animations at the same time causes performance drop. + // Show the setup dialog with delayed animation. + activity.getOverlayManager().showSetupDialog( + activity.getResources().getInteger(R.integer.side_panel_anim_short_duration) + + ADDITIONAL_DELAY_TO_SHOW_SETUP_DIALOG_MILLIS); } }); return items; diff --git a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java index 15b9d8c0..85050dc4 100644 --- a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java +++ b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java @@ -29,6 +29,7 @@ import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.data.Channel; import com.android.tv.data.ChannelNumber; +import com.android.tv.ui.OnRepeatedKeyInterceptListener; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -54,27 +55,6 @@ public class CustomizeChannelListFragment extends SideFragment { private final List mItems = new ArrayList<>(); - private final VerticalGridView.OnKeyInterceptListener mOnKeyInterceptListener = - new VerticalGridView.OnKeyInterceptListener() { - @Override - public boolean onInterceptKeyEvent(KeyEvent event) { - // In order to send tune operation once for continuous channel up/down events, we only - // call the moveToChannel method on ACTION_UP event of channel switch keys. - if (event.getAction() == KeyEvent.ACTION_UP) { - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_DOWN: - if (mLastFocusedChannelId != Channel.INVALID_ID) { - getMainActivity().tuneToChannel( - getChannelDataManager().getChannel(mLastFocusedChannelId)); - } - break; - } - } - return false; - } - }; - public CustomizeChannelListFragment() { this(Channel.INVALID_ID); } @@ -95,7 +75,25 @@ public class CustomizeChannelListFragment extends SideFragment { Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); VerticalGridView listView = (VerticalGridView) view.findViewById(R.id.side_panel_list); - listView.setOnKeyInterceptListener(mOnKeyInterceptListener); + listView.setOnKeyInterceptListener(new OnRepeatedKeyInterceptListener(listView) { + @Override + public boolean onInterceptKeyEvent(KeyEvent event) { + // In order to send tune operation once for continuous channel up/down events, + // we only call the moveToChannel method on ACTION_UP event of channel switch keys. + if (event.getAction() == KeyEvent.ACTION_UP) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + if (mLastFocusedChannelId != Channel.INVALID_ID) { + getMainActivity().tuneToChannel( + getChannelDataManager().getChannel(mLastFocusedChannelId)); + } + break; + } + } + return super.onInterceptKeyEvent(event); + } + }); if (!mGroupByFragmentRunning) { getMainActivity().startShrunkenTvView(false, true); diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java index 41e2ec37..ede5c268 100644 --- a/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java +++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java @@ -31,6 +31,7 @@ import android.widget.TextView; import com.android.tv.R; import com.android.tv.data.Channel; import com.android.tv.data.ChannelNumber; +import com.android.tv.ui.OnRepeatedKeyInterceptListener; import com.android.tv.ui.sidepanel.ActionItem; import com.android.tv.ui.sidepanel.ChannelCheckItem; import com.android.tv.ui.sidepanel.DividerItem; @@ -57,27 +58,6 @@ public class ChannelsBlockedFragment extends SideFragment { private final Item mLockAllItem = new BlockAllItem(); private final List mItems = new ArrayList<>(); - private final VerticalGridView.OnKeyInterceptListener mOnKeyInterceptListener = - new VerticalGridView.OnKeyInterceptListener() { - @Override - public boolean onInterceptKeyEvent(KeyEvent event) { - // In order to send tune operation once for continuous channel up/down events, we only - // call the moveToChannel method on ACTION_UP event of channel switch keys. - if (event.getAction() == KeyEvent.ACTION_UP) { - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_DOWN: - if (mLastFocusedChannelId != Channel.INVALID_ID) { - getMainActivity().tuneToChannel( - getChannelDataManager().getChannel(mLastFocusedChannelId)); - } - break; - } - } - return false; - } - }; - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -86,7 +66,25 @@ public class ChannelsBlockedFragment extends SideFragment { setSelectedPosition(mSelectedPosition); } VerticalGridView listView = (VerticalGridView) view.findViewById(R.id.side_panel_list); - listView.setOnKeyInterceptListener(mOnKeyInterceptListener); + listView.setOnKeyInterceptListener(new OnRepeatedKeyInterceptListener(listView) { + @Override + public boolean onInterceptKeyEvent(KeyEvent event) { + // In order to send tune operation once for continuous channel up/down events, + // we only call the moveToChannel method on ACTION_UP event of channel switch keys. + if (event.getAction() == KeyEvent.ACTION_UP) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + if (mLastFocusedChannelId != Channel.INVALID_ID) { + getMainActivity().tuneToChannel( + getChannelDataManager().getChannel(mLastFocusedChannelId)); + } + break; + } + } + return super.onInterceptKeyEvent(event); + } + }); getActivity().getContentResolver().registerContentObserver(TvContract.Programs.CONTENT_URI, true, mProgramUpdateObserver); getMainActivity().startShrunkenTvView(true, true); diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java index 73f45638..82d42377 100644 --- a/src/com/android/tv/util/AsyncDbTask.java +++ b/src/com/android/tv/util/AsyncDbTask.java @@ -21,7 +21,9 @@ import android.database.Cursor; import android.media.tv.TvContract; import android.net.Uri; import android.os.AsyncTask; +import android.support.annotation.MainThread; import android.support.annotation.NonNull; +import android.support.annotation.WorkerThread; import android.util.Log; import android.util.Range; @@ -168,6 +170,7 @@ public abstract class AsyncDbTask * *

Note This is executed on the DB thread by {@link #doInBackground(Void...)} */ + @WorkerThread protected abstract Result onQuery(Cursor c); @Override @@ -215,6 +218,7 @@ public abstract class AsyncDbTask * * @param c The cursor with the values to create T from. */ + @WorkerThread protected abstract T fromCursor(Cursor c); } @@ -238,6 +242,7 @@ public abstract class AsyncDbTask * Execute the task on the {@link #DB_EXECUTOR} thread. */ @SafeVarargs + @MainThread public final void executeOnDbThread(Params... params) { executeOnExecutor(DB_EXECUTOR, params); } diff --git a/src/com/android/tv/util/BitmapUtils.java b/src/com/android/tv/util/BitmapUtils.java index c06eac03..fd07507a 100644 --- a/src/com/android/tv/util/BitmapUtils.java +++ b/src/com/android/tv/util/BitmapUtils.java @@ -233,7 +233,7 @@ public class BitmapUtils { || size.bottom >= bitmap.getHeight() * 2); if (DEBUG) { Log.d(TAG, "needToReload(" + reqWidth + ", " + reqHeight + ")=" + reload - + " becuase the new size would be " + size + " for " + this); + + " because the new size would be " + size + " for " + this); } return reload; } diff --git a/src/com/android/tv/util/BooleanSystemProperty.java b/src/com/android/tv/util/BooleanSystemProperty.java index 11dd5ab8..6786868e 100644 --- a/src/com/android/tv/util/BooleanSystemProperty.java +++ b/src/com/android/tv/util/BooleanSystemProperty.java @@ -24,8 +24,13 @@ import java.util.List; /** * Lazy loaded boolean system property. - *

- * Set with adb shell setprop key value where value is + * + *

Set with adb shell setprop key value where: + * Values 'n', 'no', '0', 'false' or 'off' are considered false. + * Values 'y', 'yes', '1', 'true' or 'on' are considered true. + * (case sensitive). See android.os.SystemProperties.getBoolean. */ public final class BooleanSystemProperty { private final static String TAG = "BooleanSystemProperty"; diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java new file mode 100644 index 00000000..f51918e9 --- /dev/null +++ b/src/com/android/tv/util/RecurringRunner.java @@ -0,0 +1,125 @@ +/* + * 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.content.SharedPreferences; +import android.os.AsyncTask; +import android.os.Handler; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import java.util.Date; + +/** + * Repeatedly executes a {@link Runnable}. + * + *

The next execution time is saved to a {@link SharedPreferences}, and used on the next start. + * The given {@link Runnable} will run in the main thread. + */ +public final class RecurringRunner { + private static final String TAG = "RecurringRunner"; + private static final boolean DEBUG = false; + + private final Handler mHandler; + private final long mIntervalMs; + private final Runnable mRunnable; + private final Context mContext; + private final String mName; + private boolean mRunning; + + public RecurringRunner(Context context, long intervalMs, Runnable runnable) { + mContext = context.getApplicationContext(); + mRunnable = runnable; + mIntervalMs = intervalMs; + if (DEBUG) Log.i(TAG, "Delaying " + (intervalMs / 1000.0) + " seconds"); + mName = runnable.getClass().getCanonicalName(); + mHandler = new Handler(mContext.getMainLooper()); + } + + public void start() { + if (mRunning) { + Utils.engThrowElseWarn(TAG, "start is called twice.", new IllegalStateException()); + return; + } + mRunning = true; + new AsyncTask() { + @Override + protected Long doInBackground(Void... params) { + return getNextRunTime(); + } + + @Override + protected void onPostExecute(Long nextRunTime) { + postAt(nextRunTime); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + public void stop() { + mRunning = false; + mHandler.removeCallbacksAndMessages(null); + } + + private void postAt(long next) { + if (!mRunning) { + return; + } + long now = System.currentTimeMillis(); + // Run it anyways even if it is in the past + if (DEBUG) Log.i(TAG, "Next run of " + mName + " at " + new Date(next)); + long delay = Math.max(next - now, 0); + boolean posted = mHandler.postDelayed(new Runnable() { + @Override + public void run() { + try { + if (DEBUG) Log.i(TAG, "Starting " + mName); + mRunnable.run(); + } catch (Exception e) { + Log.w(TAG, "Error running " + mName, e); + } + postAt(resetNextRunTime()); + } + }, delay); + if (!posted) { + Log.w(TAG, "Scheduling a future run of " + mName + " at " + new Date(next) + "failed"); + } + if (DEBUG) Log.i(TAG, "Actual delay is " + (delay / 1000.0) + " seconds."); + } + + private SharedPreferences getSharedPreferences() { + return mContext.getSharedPreferences(RecurringRunner.class.getCanonicalName(), + Context.MODE_PRIVATE); + } + + @WorkerThread + private long getNextRunTime() { + // The access to SharedPreferences is done by an AsyncTask thread because + // SharedPreferences reads to disk at first time. + long next = getSharedPreferences().getLong(mName, System.currentTimeMillis()); + if (next > System.currentTimeMillis() + mIntervalMs) { + next = resetNextRunTime(); + } + return next; + } + + private long resetNextRunTime() { + long next = System.currentTimeMillis() + mIntervalMs; + getSharedPreferences().edit().putLong(mName, next).apply(); + return next; + } +} diff --git a/src/com/android/tv/util/SearchManagerHelper.java b/src/com/android/tv/util/SearchManagerHelper.java index 3a3f82f3..bd5db6ec 100644 --- a/src/com/android/tv/util/SearchManagerHelper.java +++ b/src/com/android/tv/util/SearchManagerHelper.java @@ -18,12 +18,11 @@ package com.android.tv.util; import android.app.SearchManager; import android.content.Context; +import android.os.Build; import android.os.Bundle; import android.os.UserHandle; import android.util.Log; -import com.android.tv.common.TvCommonConstants; - import java.lang.reflect.InvocationTargetException; /** @@ -51,25 +50,20 @@ public final class SearchManagerHelper { } } - public boolean launchAssistAction() { + public void launchAssistAction() { try { - if (TvCommonConstants.IS_MNC_PREVIEW) { - return (boolean) SearchManager.class.getDeclaredMethod( - "launchAssistAction", String.class, Integer.TYPE, Bundle.class).invoke( - mSearchManager, null, UserHandle.myUserId(), null); - } else if (TvCommonConstants.IS_MNC_OR_HIGHER) { - return (boolean) SearchManager.class.getDeclaredMethod( + if (Build.VERSION.SDK_INT >= 23) { + SearchManager.class.getDeclaredMethod( "launchLegacyAssist", String.class, Integer.TYPE, Bundle.class).invoke( mSearchManager, null, UserHandle.myUserId(), null); } else { - return (boolean) SearchManager.class.getDeclaredMethod( + SearchManager.class.getDeclaredMethod( "launchAssistAction", Integer.TYPE, String.class, Integer.TYPE).invoke( mSearchManager, 0, null, UserHandle.myUserId()); } } catch (NoSuchMethodException | IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { Log.e(TAG, "Fail to call SearchManager.launchAssistAction", e); - return false; } } } diff --git a/src/com/android/tv/util/SystemProperties.java b/src/com/android/tv/util/SystemProperties.java index 6f661976..88266cad 100644 --- a/src/com/android/tv/util/SystemProperties.java +++ b/src/com/android/tv/util/SystemProperties.java @@ -20,6 +20,25 @@ package com.android.tv.util; * A convenience class for getting TV related system properties. */ public final class SystemProperties { + + /** + * Allow Google Analytics for eng builds. + */ + public static final BooleanSystemProperty ALLOW_ANALYTICS_IN_ENG = new BooleanSystemProperty( + "tv_allow_analytics_in_eng", false); + + /** + * Allow Strict mode for debug builds. + */ + public static final BooleanSystemProperty ALLOW_STRICT_MODE = new BooleanSystemProperty( + "tv_allow_strict_mode", true); + + /** + * Allow Strict death penalty for eng builds. + */ + public static final BooleanSystemProperty ALLOW_DEATH_PENALTY = new BooleanSystemProperty( + "tv_allow_death_penalty", true); + /** * When true {@link android.view.KeyEvent}s are logged. Defaults to false. */ diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java index 63d62697..66c0ba81 100644 --- a/src/com/android/tv/util/TvInputManagerHelper.java +++ b/src/com/android/tv/util/TvInputManagerHelper.java @@ -47,60 +47,61 @@ public class TvInputManagerHelper { // Bundled (system) inputs not in the list will get the high priority // so they and their channels come first in the UI. private static final Set BUNDLED_PACKAGE_SET = new HashSet<>(); + static { BUNDLED_PACKAGE_SET.add("com.android.tv"); BUNDLED_PACKAGE_SET.add("com.android.tv"); - }; + BUNDLED_PACKAGE_SET.add("com.google.android.usbtuner"); + } private final Context mContext; private final TvInputManager mTvInputManager; private final Map mInputStateMap = new HashMap<>(); private final Map mInputMap = new HashMap<>(); private final Map mInputIdToPartnerInputMap = new HashMap<>(); - private final TvInputCallback mInternalCallback = - new TvInputCallback() { - @Override - public void onInputStateChanged(String inputId, int state) { - mInputStateMap.put(inputId, state); - for (TvInputCallback callback : mCallbacks) { - callback.onInputStateChanged(inputId, state); - } - } - - @Override - public void onInputAdded(String inputId) { - TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); - if (info != null) { - mInputMap.put(inputId, info); - mInputStateMap.put(inputId, mTvInputManager.getInputState(inputId)); - mInputIdToPartnerInputMap.put(inputId, isPartnerInput(info)); - } - mContentRatingsManager.update(); - for (TvInputCallback callback : mCallbacks) { - callback.onInputAdded(inputId); - } - } - - @Override - public void onInputRemoved(String inputId) { - mInputMap.remove(inputId); - mInputStateMap.remove(inputId); - mInputIdToPartnerInputMap.remove(inputId); - mContentRatingsManager.update(); - for (TvInputCallback callback : mCallbacks) { - callback.onInputRemoved(inputId); - } - } - - @Override - public void onInputUpdated(String inputId) { - TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); - mInputMap.put(inputId, info); - for (TvInputCallback callback : mCallbacks) { - callback.onInputUpdated(inputId); - } - } - }; + private final TvInputCallback mInternalCallback = new TvInputCallback() { + @Override + public void onInputStateChanged(String inputId, int state) { + mInputStateMap.put(inputId, state); + for (TvInputCallback callback : mCallbacks) { + callback.onInputStateChanged(inputId, state); + } + } + + @Override + public void onInputAdded(String inputId) { + TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); + if (info != null) { + mInputMap.put(inputId, info); + mInputStateMap.put(inputId, mTvInputManager.getInputState(inputId)); + mInputIdToPartnerInputMap.put(inputId, isPartnerInput(info)); + } + mContentRatingsManager.update(); + for (TvInputCallback callback : mCallbacks) { + callback.onInputAdded(inputId); + } + } + + @Override + public void onInputRemoved(String inputId) { + mInputMap.remove(inputId); + mInputStateMap.remove(inputId); + mInputIdToPartnerInputMap.remove(inputId); + mContentRatingsManager.update(); + for (TvInputCallback callback : mCallbacks) { + callback.onInputRemoved(inputId); + } + } + + @Override + public void onInputUpdated(String inputId) { + TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); + mInputMap.put(inputId, info); + for (TvInputCallback callback : mCallbacks) { + callback.onInputUpdated(inputId); + } + } + }; private final Handler mHandler = new Handler(); private boolean mStarted; @@ -178,15 +179,30 @@ public class TvInputManagerHelper { * Checks if the input is from a partner. * * It's visible for comparator test. - * Package private is enough for this method, but public is necessary to workaround mockito bug. + * Package private is enough for this method, but public is necessary to workaround mockito + * bug. */ @VisibleForTesting public boolean isPartnerInput(TvInputInfo inputInfo) { + return isSystemInput(inputInfo) && !isBundledInput(inputInfo); + } + + /** + * Does the input have {@link ApplicationInfo#FLAG_SYSTEM} set. + */ + public boolean isSystemInput(TvInputInfo inputInfo) { return inputInfo != null && (inputInfo.getServiceInfo().applicationInfo.flags - & ApplicationInfo.FLAG_SYSTEM) != 0 - && !BUNDLED_PACKAGE_SET.contains( - inputInfo.getServiceInfo().applicationInfo.packageName); + & ApplicationInfo.FLAG_SYSTEM) != 0; + } + + /** + * Is the input one known bundled inputs not written by OEM/SOCs. + */ + public boolean isBundledInput(TvInputInfo inputInfo) { + return inputInfo != null + && BUNDLED_PACKAGE_SET.contains( + inputInfo.getServiceInfo().applicationInfo.packageName); } /** @@ -202,7 +218,8 @@ public class TvInputManagerHelper { * Loads label of {@param info}. * * It's visible for comparator test to mock TvInputInfo. - * Package private is enough for this method, but public is necessary to workaround mockito bug. + * Package private is enough for this method, but public is necessary to workaround mockito + * bug. */ @VisibleForTesting public String loadLabel(TvInputInfo info) { @@ -214,7 +231,8 @@ public class TvInputManagerHelper { */ public boolean hasTvInputInfo(String inputId) { if (!mStarted) { - Log.w(TAG, "hasTvInputInfo() called before TvInputManagerHelper was started."); + Utils.engThrowElseWarn(TAG, + "hasTvInputInfo() called before TvInputManagerHelper was started."); return false; } return !TextUtils.isEmpty(inputId) && mInputMap.get(inputId) != null; @@ -222,7 +240,8 @@ public class TvInputManagerHelper { public TvInputInfo getTvInputInfo(String inputId) { if (!mStarted) { - Log.w(TAG, "getTvInputInfo() called before TvInputManagerHelper was started."); + Utils.engThrowElseWarn(TAG, + "getTvInputInfo() called before TvInputManagerHelper was started."); return null; } if (inputId == null) { @@ -291,7 +310,7 @@ public class TvInputManagerHelper { */ @VisibleForTesting static class TvInputInfoComparator implements Comparator { - private TvInputManagerHelper mInputManager; + private final TvInputManagerHelper mInputManager; public TvInputInfoComparator(TvInputManagerHelper inputManager) { mInputManager = inputManager; @@ -304,5 +323,5 @@ public class TvInputManagerHelper { } return mInputManager.loadLabel(lhs).compareTo(mInputManager.loadLabel(rhs)); } - }; + } } diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java index b0822517..a0ed0924 100644 --- a/src/com/android/tv/util/Utils.java +++ b/src/com/android/tv/util/Utils.java @@ -32,11 +32,13 @@ import android.net.Uri; import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.Log; import android.view.View; +import com.android.tv.BuildConfig; import com.android.tv.R; import com.android.tv.data.Channel; import com.android.tv.data.Program; @@ -124,6 +126,7 @@ public class Utils { return sb.toString(); } + @WorkerThread public static String getInputIdForChannel(Context context, long channelId) { if (channelId == Channel.INVALID_ID) { return null; @@ -219,6 +222,7 @@ public class Utils { /** * Gets the info of the program on particular time. */ + @WorkerThread public static Program getProgramAt(Context context, long channelId, long timeMs) { if (channelId == Channel.INVALID_ID) { Log.e(TAG, "getCurrentProgramAt - channelId is invalid"); @@ -247,6 +251,7 @@ public class Utils { /** * Gets the info of the current program. */ + @WorkerThread public static Program getCurrentProgram(Context context, long channelId) { return getProgramAt(context, channelId, System.currentTimeMillis()); } @@ -265,8 +270,8 @@ public class Utils { */ public static String getDurationString( Context context, long startUtcMillis, long endUtcMillis, boolean useShortFormat) { - return getDurationString(context, System.currentTimeMillis(), - startUtcMillis, endUtcMillis, useShortFormat, 0); + return getDurationString(context, System.currentTimeMillis(), startUtcMillis, endUtcMillis, + useShortFormat, 0); } @VisibleForTesting @@ -495,6 +500,7 @@ public class Utils { /** * Enable all channels synchronously. */ + @WorkerThread public static void enableAllChannels(Context context) { ContentValues values = new ContentValues(); values.put(Channels.COLUMN_BROWSABLE, 1); @@ -544,4 +550,48 @@ public class Utils { public static String intern(@Nullable String string) { return string == null ? null : string.intern(); } + + /** + * Checks if this application is running in tests. + * + *

{@link android.app.ActivityManager#isRunningInTestHarness} doesn't return {@code true} for + * the usual devices even the application is running in tests. We need to figure it out by + * checking whether the class in tv-tests-common module can be loaded or not. + */ + public static boolean isRunningInTest() { + try { + Class.forName("com.android.tv.testing.Utils"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + /** + * Throws a {@link RuntimeException} if {@link BuildConfig#ENG} is true, else log a warning. + * + * @param tag Used to log message. + * @param msg The message + */ + public static void engThrowElseWarn(String tag, String msg) { + if (BuildConfig.ENG) { + throw new RuntimeException(msg); + } else { + Log.w(tag, msg); + } + } + + /** + * Throws a {@link RuntimeException} if {@link BuildConfig#ENG} is true, else log a warning. + * + * @param tag Used to log message. + * @param msg The message + */ + public static void engThrowElseWarn(String tag, String msg, RuntimeException e) { + if (BuildConfig.ENG) { + throw e; + } else { + Log.w(tag, msg); + } + } } -- cgit v1.2.3