1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package android.support.v17.leanback.app; 17 18import android.content.Context; 19import android.graphics.drawable.Drawable; 20import android.os.Bundle; 21import android.os.Handler; 22import android.support.v17.leanback.media.PlaybackControlGlue; 23import android.support.v17.leanback.test.R; 24import android.support.v17.leanback.widget.Action; 25import android.support.v17.leanback.widget.ArrayObjectAdapter; 26import android.support.v17.leanback.widget.ClassPresenterSelector; 27import android.support.v17.leanback.widget.HeaderItem; 28import android.support.v17.leanback.widget.ListRow; 29import android.support.v17.leanback.widget.ListRowPresenter; 30import android.support.v17.leanback.widget.OnItemViewClickedListener; 31import android.support.v17.leanback.widget.PlaybackControlsRow; 32import android.support.v17.leanback.widget.Presenter; 33import android.support.v17.leanback.widget.PresenterSelector; 34import android.support.v17.leanback.widget.Row; 35import android.support.v17.leanback.widget.RowPresenter; 36import android.support.v17.leanback.widget.SparseArrayObjectAdapter; 37import android.util.Log; 38import android.view.KeyEvent; 39import android.view.View; 40import android.widget.Toast; 41 42public class PlaybackTestFragment extends PlaybackFragment { 43 private static final String TAG = "PlaybackTestFragment"; 44 45 /** 46 * Change this to choose a different overlay background. 47 */ 48 private static final int BACKGROUND_TYPE = PlaybackFragment.BG_LIGHT; 49 50 /** 51 * Change this to select hidden 52 */ 53 private static final boolean SECONDARY_HIDDEN = false; 54 55 /** 56 * Change the number of related content rows. 57 */ 58 private static final int RELATED_CONTENT_ROWS = 3; 59 60 private android.support.v17.leanback.media.PlaybackControlGlue mGlue; 61 boolean mDestroyCalled; 62 63 @Override 64 public SparseArrayObjectAdapter getAdapter() { 65 return (SparseArrayObjectAdapter) super.getAdapter(); 66 } 67 68 private OnItemViewClickedListener mOnItemViewClickedListener = new OnItemViewClickedListener() { 69 @Override 70 public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, 71 RowPresenter.ViewHolder rowViewHolder, Row row) { 72 Log.d(TAG, "onItemClicked: " + item + " row " + row); 73 } 74 }; 75 76 @Override 77 public void onDestroy() { 78 super.onDestroy(); 79 mDestroyCalled = true; 80 } 81 82 @Override 83 public void onCreate(Bundle savedInstanceState) { 84 Log.i(TAG, "onCreate"); 85 super.onCreate(savedInstanceState); 86 87 setBackgroundType(BACKGROUND_TYPE); 88 89 createComponents(getActivity()); 90 setOnItemViewClickedListener(mOnItemViewClickedListener); 91 } 92 93 private void createComponents(Context context) { 94 mGlue = new PlaybackControlHelper(context) { 95 @Override 96 public int getUpdatePeriod() { 97 long totalTime = getControlsRow().getDuration(); 98 if (getView() == null || getView().getWidth() == 0 || totalTime <= 0) { 99 return 1000; 100 } 101 return 16; 102 } 103 104 @Override 105 public void onActionClicked(Action action) { 106 if (action.getId() == R.id.lb_control_picture_in_picture) { 107 getActivity().enterPictureInPictureMode(); 108 return; 109 } 110 super.onActionClicked(action); 111 } 112 113 @Override 114 protected void onCreateControlsRowAndPresenter() { 115 super.onCreateControlsRowAndPresenter(); 116 getControlsRowPresenter().setSecondaryActionsHidden(SECONDARY_HIDDEN); 117 } 118 }; 119 120 mGlue.setHost(new PlaybackFragmentGlueHost(this)); 121 ClassPresenterSelector selector = new ClassPresenterSelector(); 122 selector.addClassPresenter(ListRow.class, new ListRowPresenter()); 123 124 setAdapter(new SparseArrayObjectAdapter(selector)); 125 126 // Add related content rows 127 for (int i = 0; i < RELATED_CONTENT_ROWS; ++i) { 128 ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new StringPresenter()); 129 listRowAdapter.add("Some related content"); 130 listRowAdapter.add("Other related content"); 131 HeaderItem header = new HeaderItem(i, "Row " + i); 132 getAdapter().set(1 + i, new ListRow(header, listRowAdapter)); 133 } 134 } 135 136 public PlaybackControlGlue getGlue() { 137 return mGlue; 138 } 139 140 abstract static class PlaybackControlHelper extends PlaybackControlGlue { 141 /** 142 * Change the location of the thumbs up/down controls 143 */ 144 private static final boolean THUMBS_PRIMARY = true; 145 146 private static final String FAUX_TITLE = "A short song of silence"; 147 private static final String FAUX_SUBTITLE = "2014"; 148 private static final int FAUX_DURATION = 33 * 1000; 149 150 // These should match the playback service FF behavior 151 private static int[] sFastForwardSpeeds = { 2, 3, 4, 5 }; 152 153 private boolean mIsPlaying; 154 private int mSpeed = PLAYBACK_SPEED_PAUSED; 155 private long mStartTime; 156 private long mStartPosition = 0; 157 158 private PlaybackControlsRow.RepeatAction mRepeatAction; 159 private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction; 160 private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction; 161 private PlaybackControlsRow.PictureInPictureAction mPipAction; 162 private static Handler sProgressHandler = new Handler(); 163 164 private final Runnable mUpdateProgressRunnable = new Runnable() { 165 @Override 166 public void run() { 167 updateProgress(); 168 sProgressHandler.postDelayed(this, getUpdatePeriod()); 169 } 170 }; 171 172 PlaybackControlHelper(Context context) { 173 super(context, sFastForwardSpeeds); 174 mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context); 175 mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.INDEX_OUTLINE); 176 mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context); 177 mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.INDEX_OUTLINE); 178 mRepeatAction = new PlaybackControlsRow.RepeatAction(context); 179 mPipAction = new PlaybackControlsRow.PictureInPictureAction(context); 180 } 181 182 @Override 183 protected SparseArrayObjectAdapter createPrimaryActionsAdapter( 184 PresenterSelector presenterSelector) { 185 SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(presenterSelector); 186 if (THUMBS_PRIMARY) { 187 adapter.set(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST, mThumbsUpAction); 188 adapter.set(PlaybackControlGlue.ACTION_CUSTOM_RIGHT_FIRST, mThumbsDownAction); 189 } 190 return adapter; 191 } 192 193 @Override 194 public void onActionClicked(Action action) { 195 if (shouldDispatchAction(action)) { 196 dispatchAction(action); 197 return; 198 } 199 super.onActionClicked(action); 200 } 201 202 @Override 203 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) { 204 if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { 205 Action action = getControlsRow().getActionForKeyCode(keyEvent.getKeyCode()); 206 if (shouldDispatchAction(action)) { 207 dispatchAction(action); 208 return true; 209 } 210 } 211 return super.onKey(view, keyCode, keyEvent); 212 } 213 214 private boolean shouldDispatchAction(Action action) { 215 return action == mRepeatAction || action == mThumbsUpAction 216 || action == mThumbsDownAction; 217 } 218 219 private void dispatchAction(Action action) { 220 Toast.makeText(getContext(), action.toString(), Toast.LENGTH_SHORT).show(); 221 PlaybackControlsRow.MultiAction multiAction = (PlaybackControlsRow.MultiAction) action; 222 multiAction.nextIndex(); 223 notifyActionChanged(multiAction); 224 } 225 226 private void notifyActionChanged(PlaybackControlsRow.MultiAction action) { 227 int index; 228 index = getPrimaryActionsAdapter().indexOf(action); 229 if (index >= 0) { 230 getPrimaryActionsAdapter().notifyArrayItemRangeChanged(index, 1); 231 } else { 232 index = getSecondaryActionsAdapter().indexOf(action); 233 if (index >= 0) { 234 getSecondaryActionsAdapter().notifyArrayItemRangeChanged(index, 1); 235 } 236 } 237 } 238 239 private SparseArrayObjectAdapter getPrimaryActionsAdapter() { 240 return (SparseArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter(); 241 } 242 243 private ArrayObjectAdapter getSecondaryActionsAdapter() { 244 return (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter(); 245 } 246 247 @Override 248 public boolean hasValidMedia() { 249 return true; 250 } 251 252 @Override 253 public boolean isMediaPlaying() { 254 return mIsPlaying; 255 } 256 257 @Override 258 public CharSequence getMediaTitle() { 259 return FAUX_TITLE; 260 } 261 262 @Override 263 public CharSequence getMediaSubtitle() { 264 return FAUX_SUBTITLE; 265 } 266 267 @Override 268 public int getMediaDuration() { 269 return FAUX_DURATION; 270 } 271 272 @Override 273 public Drawable getMediaArt() { 274 return null; 275 } 276 277 @Override 278 public long getSupportedActions() { 279 return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND; 280 } 281 282 @Override 283 public int getCurrentSpeedId() { 284 return mSpeed; 285 } 286 287 @Override 288 public int getCurrentPosition() { 289 int speed; 290 if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_PAUSED) { 291 speed = 0; 292 } else if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_NORMAL) { 293 speed = 1; 294 } else if (mSpeed >= PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) { 295 int index = mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0; 296 speed = getFastForwardSpeeds()[index]; 297 } else if (mSpeed <= -PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) { 298 int index = -mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0; 299 speed = -getRewindSpeeds()[index]; 300 } else { 301 return -1; 302 } 303 long position = mStartPosition + (System.currentTimeMillis() - mStartTime) * speed; 304 if (position > getMediaDuration()) { 305 position = getMediaDuration(); 306 onPlaybackComplete(true); 307 } else if (position < 0) { 308 position = 0; 309 onPlaybackComplete(false); 310 } 311 return (int) position; 312 } 313 314 void onPlaybackComplete(final boolean ended) { 315 sProgressHandler.post(new Runnable() { 316 @Override 317 public void run() { 318 if (mRepeatAction.getIndex() == PlaybackControlsRow.RepeatAction.INDEX_NONE) { 319 pause(); 320 } else { 321 play(PlaybackControlGlue.PLAYBACK_SPEED_NORMAL); 322 } 323 mStartPosition = 0; 324 onStateChanged(); 325 } 326 }); 327 } 328 329 @Override 330 public void play(int speed) { 331 if (speed == mSpeed) { 332 return; 333 } 334 mStartPosition = getCurrentPosition(); 335 mSpeed = speed; 336 mIsPlaying = true; 337 mStartTime = System.currentTimeMillis(); 338 } 339 340 @Override 341 public void pause() { 342 if (mSpeed == PLAYBACK_SPEED_PAUSED) { 343 return; 344 } 345 mStartPosition = getCurrentPosition(); 346 mSpeed = PLAYBACK_SPEED_PAUSED; 347 mIsPlaying = false; 348 } 349 350 @Override 351 public void next() { 352 // Not supported 353 } 354 355 @Override 356 public void previous() { 357 // Not supported 358 } 359 360 @Override 361 public void enableProgressUpdating(boolean enable) { 362 sProgressHandler.removeCallbacks(mUpdateProgressRunnable); 363 if (enable) { 364 mUpdateProgressRunnable.run(); 365 } 366 } 367 } 368} 369