aboutsummaryrefslogtreecommitdiff
path: root/src/com/android
diff options
context:
space:
mode:
authorNick Chalko <nchalko@google.com>2015-09-01 09:05:04 -0700
committerNick Chalko <nchalko@google.com>2015-09-16 06:46:50 -0700
commit07b043dc3db83d6d20f0e8513b946830ab00e37b (patch)
tree705ade719e5c2853c070fe40b8518a56ac37f6d0 /src/com/android
parentb5429e4406a580953bbdac5817e421cf0ab7aae3 (diff)
downloadTV-07b043dc3db83d6d20f0e8513b946830ab00e37b.tar.gz
Sync to ub-tv-friends at 1.06.202
git hash 3c1965f5dcc60243f1fe600cb35f19bd5f01fc27 Change-Id: I90b77790f9074677ecef72a23235d2b33eacb76a
Diffstat (limited to 'src/com/android')
-rw-r--r--src/com/android/tv/ChannelTuner.java2
-rw-r--r--src/com/android/tv/LauncherActivity.java4
-rw-r--r--src/com/android/tv/MainActivity.java221
-rw-r--r--src/com/android/tv/TimeShiftManager.java49
-rw-r--r--src/com/android/tv/TvApplication.java71
-rw-r--r--src/com/android/tv/analytics/HasTrackerLabel.java2
-rw-r--r--src/com/android/tv/analytics/StubTracker.java16
-rw-r--r--src/com/android/tv/analytics/Tracker.java42
-rw-r--r--src/com/android/tv/customization/CustomAction.java9
-rw-r--r--src/com/android/tv/data/Channel.java195
-rw-r--r--src/com/android/tv/data/ChannelDataManager.java56
-rw-r--r--src/com/android/tv/data/ChannelLogoFetcher.java22
-rw-r--r--src/com/android/tv/data/DisplayMode.java5
-rw-r--r--src/com/android/tv/data/Program.java104
-rw-r--r--src/com/android/tv/data/ProgramDataManager.java17
-rw-r--r--src/com/android/tv/data/StreamInfo.java2
-rw-r--r--src/com/android/tv/dialog/PinDialogFragment.java4
-rw-r--r--src/com/android/tv/dialog/SafeDismissDialogFragment.java1
-rw-r--r--src/com/android/tv/guide/ProgramGrid.java36
-rw-r--r--src/com/android/tv/guide/ProgramGuide.java98
-rw-r--r--src/com/android/tv/guide/ProgramItemView.java12
-rw-r--r--src/com/android/tv/guide/ProgramManager.java21
-rw-r--r--src/com/android/tv/menu/AppLinkCardView.java4
-rw-r--r--src/com/android/tv/menu/ChannelsPosterPrefetcher.java52
-rw-r--r--src/com/android/tv/menu/ChannelsRow.java24
-rw-r--r--src/com/android/tv/menu/ChannelsRowAdapter.java4
-rw-r--r--src/com/android/tv/menu/IMenuView.java60
-rw-r--r--src/com/android/tv/menu/ItemListRow.java8
-rw-r--r--src/com/android/tv/menu/Menu.java330
-rw-r--r--src/com/android/tv/menu/MenuLayoutManager.java819
-rw-r--r--src/com/android/tv/menu/MenuRow.java34
-rw-r--r--src/com/android/tv/menu/MenuRowFactory.java118
-rw-r--r--src/com/android/tv/menu/MenuRowView.java237
-rw-r--r--src/com/android/tv/menu/MenuView.java691
-rw-r--r--src/com/android/tv/menu/PlayControlsRow.java20
-rw-r--r--src/com/android/tv/menu/PlayControlsRowView.java50
-rw-r--r--src/com/android/tv/menu/TvOptionsRowAdapter.java4
-rw-r--r--src/com/android/tv/receiver/AudioCapabilitiesReceiver.java29
-rw-r--r--src/com/android/tv/recommendation/NotificationService.java140
-rw-r--r--src/com/android/tv/recommendation/RecommendationDataManager.java129
-rw-r--r--src/com/android/tv/recommendation/RoutineWatchEvaluator.java2
-rw-r--r--src/com/android/tv/search/TvProviderSearch.java13
-rw-r--r--src/com/android/tv/ui/ChannelBannerView.java6
-rw-r--r--src/com/android/tv/ui/FullscreenDialogView.java106
-rw-r--r--src/com/android/tv/ui/InputBannerView.java6
-rw-r--r--src/com/android/tv/ui/IntroView.java36
-rw-r--r--src/com/android/tv/ui/KeypadChannelSwitchView.java6
-rw-r--r--src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java112
-rw-r--r--src/com/android/tv/ui/SelectInputView.java6
-rw-r--r--src/com/android/tv/ui/SetupView.java178
-rw-r--r--src/com/android/tv/ui/TunableTvView.java7
-rw-r--r--src/com/android/tv/ui/TvOverlayManager.java206
-rw-r--r--src/com/android/tv/ui/TvViewUiManager.java20
-rw-r--r--src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java9
-rw-r--r--src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java42
-rw-r--r--src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java42
-rw-r--r--src/com/android/tv/util/AsyncDbTask.java5
-rw-r--r--src/com/android/tv/util/BitmapUtils.java2
-rw-r--r--src/com/android/tv/util/BooleanSystemProperty.java9
-rw-r--r--src/com/android/tv/util/RecurringRunner.java125
-rw-r--r--src/com/android/tv/util/SearchManagerHelper.java16
-rw-r--r--src/com/android/tv/util/SystemProperties.java19
-rw-r--r--src/com/android/tv/util/TvInputManagerHelper.java127
-rw-r--r--src/com/android/tv/util/Utils.java54
64 files changed, 3283 insertions, 1613 deletions
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<String> 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<String> 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<MainActivity> {
+ 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<TimeShiftManager> {
+ 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<TvInputInfo> 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,13 +16,24 @@
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() { }
@Override
@@ -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.
+ *
+ * <p>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.
+ *
+ * <p>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<CustomAction> {
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<String> 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.
+ *
+ * <p>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<Runnable> mPostRunnablesAfterChannelUpdate = new ArrayList<>();
+ // TODO: move ChannelDataManager to TvApplication to consistently run mRecurringRunner.
+ private RecurringRunner mRecurringRunner;
+ private final Tracker mTracker;
private final Set<Listener> mListeners = new HashSet<>();
private final Map<Long, ChannelWrapper> 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<ChannelDataManager> {
+ 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<String, String> 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<Program> {
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<Program> {
TvContract.Programs.COLUMN_VIDEO_HEIGHT
};
+ /**
+ * Creates {@code Program} object from cursor.
+ *
+ * <p>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<Program> {
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<Program> onQuery(Cursor c) {
final List<Program> 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<Program> 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<View> 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.
+ *
+ * <p>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<ProgramGuide> {
+ 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<Channel> 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<Channel> 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<ChannelsPosterPrefetcher> {
+ 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<Channel>
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<MenuRow> menuRows);
+
+ /**
+ * Shows the main menu.
+ *
+ * <p> 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.
+ *
+ * <p>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<String> 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<Menu> 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<MenuRow> 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.
+ *
+ * <p>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<Menu> {
+ 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<MenuRow> mMenuRows = new ArrayList<>();
+ private final List<MenuRowView> mMenuRowViews = new ArrayList<>();
+ private final List<Integer> 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<ViewPropertyValueHolder> 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<MenuRow> menuRows, List<MenuRowView> menuRowViews) {
+ mMenuRows.clear();
+ mMenuRows.addAll(menuRows);
+ mMenuRowViews.clear();
+ mMenuRowViews.addAll(menuRowViews);
+ }
+
+ /**
+ * Layouts main menu view.
+ *
+ * <p>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<Rect> 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<Integer> rowsToAdd,
+ List<Integer> 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<Rect> getViewLayouts(int left, int top, int right, int bottom) {
+ return getViewLayouts(left, top, right, bottom, Collections.<Integer>emptyList(),
+ Collections.<Integer>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).
+ *
+ * <p>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<Rect> getViewLayouts(int left, int top, int right, int bottom,
+ List<Integer> rowsToAdd, List<Integer> rowsToRemove) {
+ // The coordinates should be relative to the parent.
+ int relativeLeft = 0;
+ int relateiveRight = right - left;
+ int relativeBottom = bottom - top;
+
+ List<Rect> 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<Animator> animators = new ArrayList<>();
+ boolean scrollDown = position > oldPosition;
+ List<Rect> 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<ViewPropertyValueHolder> 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.
+ *
+ * <p>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<Integer> addedRowViews = new ArrayList<>();
+ List<Integer> removedRowViews = new ArrayList<>();
+ Map<Integer, Integer> 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<Animator> animators = new ArrayList<>();
+ List<Rect> 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<Integer, Integer> entry : offsetsToMove.entrySet()) {
+ MenuRowView view = mMenuRowViews.get(entry.getKey());
+ animators.add(createTranslationYAnimator(view, 0, entry.getValue() * mRowTitleHeight));
+ }
+ // Run animation.
+ final List<ViewPropertyValueHolder> 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<View, Float> property;
+ public View view;
+ public float value;
+
+ public ViewPropertyValueHolder(Property<View, Float> 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<CustomAction> 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<CustomAction> 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<CustomAction> 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<String> 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<MenuRow> mMenuRows = new ArrayList<>();
+ private final List<MenuRowView> 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<MenuRow> itemList = new ArrayList<>();
- itemList.add(new PlayControlsRow(context));
- itemList.add(mChannelsRow = new ChannelsRow(context));
- List<CustomAction> 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<MenuRow> 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.
- *
- * <p>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.
- *
- * <p>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<MenuViewHolder> {
- private List<MenuRow> mAllItems = Collections.emptyList();
- private List<MenuRow> mVisibleItems = new ArrayList<>();
-
- private void setItemList(List<MenuRow> 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.<MenuRow>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<MenuRow> 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<CustomAction> 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<CustomAction> 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<TvTrackInfo> 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<NotificationService> {
+ 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<Long, ChannelRecord> mChannelRecordMap = new ConcurrentHashMap<>();
private final Map<Long, ChannelRecord> 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<ListenerRecord> 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<Channel> 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<WatchedProgram> 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<RecommendationDataManager> {
+ 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<SearchResult> search(String query, int limit, int action) {
List<SearchResult> results = new ArrayList<>();
Set<Long> 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<SearchResult> searchChannels(String query, Set<Long> channels, int limit) {
List<SearchResult> results = new ArrayList<>();
if (TextUtils.isDigitsOnly(query)) {
@@ -174,6 +176,7 @@ public class TvProviderSearch {
return results;
}
+ @WorkerThread
private List<SearchResult> searchChannels(String query, String[] columnForExactMatching,
String[] columnForPartialMatching, Set<Long> 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<SearchResult> searchPrograms(String query, String[] columnForExactMatching,
String[] columnForPartialMatching, Set<Long> channelsFound, int limit) {
Assert.assertTrue(
@@ -459,6 +464,8 @@ public class TvProviderSearch {
return result;
}
+
+ @WorkerThread
private class ChannelComparatorWithSameDisplayNumber implements Comparator<SearchResult> {
private final Map<Long, Long> 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<OnRepeatedKeyInterceptListener> {
+ 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<TvInputInfo> 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
@@ -106,6 +148,18 @@ public class SetupView extends FullscreenDialogView {
}
@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<Animator> 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<Animator> 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<MyViewHolder> {
@Override
public int getItemViewType(int position) {
@@ -286,8 +420,8 @@ public class SetupView extends FullscreenDialogView {
@VisibleForTesting
static class TvInputInfoComparator implements Comparator<TvInputInfo> {
- 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<TvTrackInfo> 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);
}
/**
@@ -304,6 +330,35 @@ public class TvOverlayManager {
}
/**
+ * 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.
*/
public void onDialogDestroyed() {
@@ -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<TvOverlayManager> {
+ 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<Item> 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<Item> 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<Params, Progress, Result>
*
* <p><b>Note</b> 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<Params, Progress, Result>
*
* @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<Params, Progress, Result>
* 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.
- * <p>
- * Set with <code>adb shell setprop <em>key</em> <em>value</em></code> where value is
+ *
+ * <p>Set with <code>adb shell setprop <em>key</em> <em>value</em></code> where:
+ * Values 'n', 'no', '0', 'false' or 'off' are considered false.
+ * Values 'y', 'yes', '1', 'true' or 'on' are considered true.
+ * (case sensitive). See <a href=
+ * "https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/SystemProperties.java"
+ * >android.os.SystemProperties.getBoolean</a>.
*/
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}.
+ *
+ * <p>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<Void, Void, Long>() {
+ @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<String> 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<String, Integer> mInputStateMap = new HashMap<>();
private final Map<String, TvInputInfo> mInputMap = new HashMap<>();
private final Map<String, Boolean> 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<TvInputInfo> {
- 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.
+ *
+ * <p>{@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);
+ }
+ }
}