1 /* 2 * Copyright (C) 2017 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 */ 16 package com.google.android.exoplayer2.castdemo; 17 18 import android.content.Context; 19 import android.os.Bundle; 20 import android.view.KeyEvent; 21 import android.view.LayoutInflater; 22 import android.view.Menu; 23 import android.view.View; 24 import android.view.View.OnClickListener; 25 import android.view.ViewGroup; 26 import android.widget.ArrayAdapter; 27 import android.widget.ListView; 28 import android.widget.TextView; 29 import android.widget.Toast; 30 import androidx.annotation.NonNull; 31 import androidx.annotation.Nullable; 32 import androidx.appcompat.app.AlertDialog; 33 import androidx.appcompat.app.AppCompatActivity; 34 import androidx.core.graphics.ColorUtils; 35 import androidx.recyclerview.widget.ItemTouchHelper; 36 import androidx.recyclerview.widget.LinearLayoutManager; 37 import androidx.recyclerview.widget.RecyclerView; 38 import androidx.recyclerview.widget.RecyclerView.ViewHolder; 39 import com.google.android.exoplayer2.C; 40 import com.google.android.exoplayer2.MediaItem; 41 import com.google.android.exoplayer2.SimpleExoPlayer; 42 import com.google.android.exoplayer2.ui.PlayerControlView; 43 import com.google.android.exoplayer2.ui.PlayerView; 44 import com.google.android.exoplayer2.util.Assertions; 45 import com.google.android.exoplayer2.util.Util; 46 import com.google.android.gms.cast.framework.CastButtonFactory; 47 import com.google.android.gms.cast.framework.CastContext; 48 import com.google.android.gms.dynamite.DynamiteModule; 49 50 /** 51 * An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's 52 * Cast extension. 53 */ 54 public class MainActivity extends AppCompatActivity 55 implements OnClickListener, PlayerManager.Listener { 56 57 private PlayerView localPlayerView; 58 private PlayerControlView castControlView; 59 private PlayerManager playerManager; 60 private RecyclerView mediaQueueList; 61 private MediaQueueListAdapter mediaQueueListAdapter; 62 private CastContext castContext; 63 64 // Activity lifecycle methods. 65 66 @Override onCreate(Bundle savedInstanceState)67 public void onCreate(Bundle savedInstanceState) { 68 super.onCreate(savedInstanceState); 69 // Getting the cast context later than onStart can cause device discovery not to take place. 70 try { 71 castContext = CastContext.getSharedInstance(this); 72 } catch (RuntimeException e) { 73 Throwable cause = e.getCause(); 74 while (cause != null) { 75 if (cause instanceof DynamiteModule.LoadingException) { 76 setContentView(R.layout.cast_context_error); 77 return; 78 } 79 cause = cause.getCause(); 80 } 81 // Unknown error. We propagate it. 82 throw e; 83 } 84 85 setContentView(R.layout.main_activity); 86 87 localPlayerView = findViewById(R.id.local_player_view); 88 localPlayerView.requestFocus(); 89 90 castControlView = findViewById(R.id.cast_control_view); 91 92 mediaQueueList = findViewById(R.id.sample_list); 93 ItemTouchHelper helper = new ItemTouchHelper(new RecyclerViewCallback()); 94 helper.attachToRecyclerView(mediaQueueList); 95 mediaQueueList.setLayoutManager(new LinearLayoutManager(this)); 96 mediaQueueList.setHasFixedSize(true); 97 mediaQueueListAdapter = new MediaQueueListAdapter(); 98 99 findViewById(R.id.add_sample_button).setOnClickListener(this); 100 } 101 102 @Override onCreateOptionsMenu(Menu menu)103 public boolean onCreateOptionsMenu(Menu menu) { 104 super.onCreateOptionsMenu(menu); 105 getMenuInflater().inflate(R.menu.menu, menu); 106 CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item); 107 return true; 108 } 109 110 @Override onResume()111 public void onResume() { 112 super.onResume(); 113 if (castContext == null) { 114 // There is no Cast context to work with. Do nothing. 115 return; 116 } 117 playerManager = 118 new PlayerManager( 119 /* listener= */ this, 120 localPlayerView, 121 castControlView, 122 /* context= */ this, 123 castContext); 124 mediaQueueList.setAdapter(mediaQueueListAdapter); 125 } 126 127 @Override onPause()128 public void onPause() { 129 super.onPause(); 130 if (castContext == null) { 131 // Nothing to release. 132 return; 133 } 134 mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount()); 135 mediaQueueList.setAdapter(null); 136 playerManager.release(); 137 playerManager = null; 138 } 139 140 // Activity input. 141 142 @Override dispatchKeyEvent(KeyEvent event)143 public boolean dispatchKeyEvent(KeyEvent event) { 144 // If the event was not handled then see if the player view can handle it. 145 return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event); 146 } 147 148 @Override onClick(View view)149 public void onClick(View view) { 150 new AlertDialog.Builder(this) 151 .setTitle(R.string.add_samples) 152 .setView(buildSampleListView()) 153 .setPositiveButton(android.R.string.ok, null) 154 .create() 155 .show(); 156 } 157 158 // PlayerManager.Listener implementation. 159 160 @Override onQueuePositionChanged(int previousIndex, int newIndex)161 public void onQueuePositionChanged(int previousIndex, int newIndex) { 162 if (previousIndex != C.INDEX_UNSET) { 163 mediaQueueListAdapter.notifyItemChanged(previousIndex); 164 } 165 if (newIndex != C.INDEX_UNSET) { 166 mediaQueueListAdapter.notifyItemChanged(newIndex); 167 } 168 } 169 170 @Override onUnsupportedTrack(int trackType)171 public void onUnsupportedTrack(int trackType) { 172 if (trackType == C.TRACK_TYPE_AUDIO) { 173 showToast(R.string.error_unsupported_audio); 174 } else if (trackType == C.TRACK_TYPE_VIDEO) { 175 showToast(R.string.error_unsupported_video); 176 } 177 } 178 179 // Internal methods. 180 showToast(int messageId)181 private void showToast(int messageId) { 182 Toast.makeText(getApplicationContext(), messageId, Toast.LENGTH_LONG).show(); 183 } 184 buildSampleListView()185 private View buildSampleListView() { 186 View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null); 187 ListView sampleList = dialogList.findViewById(R.id.sample_list); 188 sampleList.setAdapter(new SampleListAdapter(this)); 189 sampleList.setOnItemClickListener( 190 (parent, view, position, id) -> { 191 playerManager.addItem(DemoUtil.SAMPLES.get(position)); 192 mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); 193 }); 194 return dialogList; 195 } 196 197 // Internal classes. 198 199 private class MediaQueueListAdapter extends RecyclerView.Adapter<QueueItemViewHolder> { 200 201 @Override 202 @NonNull onCreateViewHolder(ViewGroup parent, int viewType)203 public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 204 TextView v = (TextView) LayoutInflater.from(parent.getContext()) 205 .inflate(android.R.layout.simple_list_item_1, parent, false); 206 return new QueueItemViewHolder(v); 207 } 208 209 @Override onBindViewHolder(QueueItemViewHolder holder, int position)210 public void onBindViewHolder(QueueItemViewHolder holder, int position) { 211 holder.item = Assertions.checkNotNull(playerManager.getItem(position)); 212 213 TextView view = holder.textView; 214 view.setText(holder.item.mediaMetadata.title); 215 // TODO: Solve coloring using the theme's ColorStateList. 216 view.setTextColor( 217 ColorUtils.setAlphaComponent( 218 view.getCurrentTextColor(), 219 position == playerManager.getCurrentItemIndex() ? 255 : 100)); 220 } 221 222 @Override getItemCount()223 public int getItemCount() { 224 return playerManager.getMediaQueueSize(); 225 } 226 227 } 228 229 private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback { 230 231 private int draggingFromPosition; 232 private int draggingToPosition; 233 RecyclerViewCallback()234 public RecyclerViewCallback() { 235 super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END); 236 draggingFromPosition = C.INDEX_UNSET; 237 draggingToPosition = C.INDEX_UNSET; 238 } 239 240 @Override onMove( @onNull RecyclerView list, RecyclerView.ViewHolder origin, RecyclerView.ViewHolder target)241 public boolean onMove( 242 @NonNull RecyclerView list, 243 RecyclerView.ViewHolder origin, 244 RecyclerView.ViewHolder target) { 245 int fromPosition = origin.getAdapterPosition(); 246 int toPosition = target.getAdapterPosition(); 247 if (draggingFromPosition == C.INDEX_UNSET) { 248 // A drag has started, but changes to the media queue will be reflected in clearView(). 249 draggingFromPosition = fromPosition; 250 } 251 draggingToPosition = toPosition; 252 mediaQueueListAdapter.notifyItemMoved(fromPosition, toPosition); 253 return true; 254 } 255 256 @Override onSwiped(RecyclerView.ViewHolder viewHolder, int direction)257 public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { 258 int position = viewHolder.getAdapterPosition(); 259 QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder; 260 if (playerManager.removeItem(queueItemHolder.item)) { 261 mediaQueueListAdapter.notifyItemRemoved(position); 262 // Update whichever item took its place, in case it became the new selected item. 263 mediaQueueListAdapter.notifyItemChanged(position); 264 } 265 } 266 267 @Override clearView(@onNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder)268 public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) { 269 super.clearView(recyclerView, viewHolder); 270 if (draggingFromPosition != C.INDEX_UNSET) { 271 QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder; 272 // A drag has ended. We reflect the media queue change in the player. 273 if (!playerManager.moveItem(queueItemHolder.item, draggingToPosition)) { 274 // The move failed. The entire sequence of onMove calls since the drag started needs to be 275 // invalidated. 276 mediaQueueListAdapter.notifyDataSetChanged(); 277 } 278 } 279 draggingFromPosition = C.INDEX_UNSET; 280 draggingToPosition = C.INDEX_UNSET; 281 } 282 } 283 284 private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener { 285 286 public final TextView textView; 287 public MediaItem item; 288 QueueItemViewHolder(TextView textView)289 public QueueItemViewHolder(TextView textView) { 290 super(textView); 291 this.textView = textView; 292 textView.setOnClickListener(this); 293 } 294 295 @Override onClick(View v)296 public void onClick(View v) { 297 playerManager.selectQueueItem(getAdapterPosition()); 298 } 299 } 300 301 private static final class SampleListAdapter extends ArrayAdapter<MediaItem> { 302 SampleListAdapter(Context context)303 public SampleListAdapter(Context context) { 304 super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); 305 } 306 307 @Override 308 @NonNull getView(int position, @Nullable View convertView, @NonNull ViewGroup parent)309 public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { 310 View view = super.getView(position, convertView, parent); 311 ((TextView) view).setText(Util.castNonNull(getItem(position)).mediaMetadata.title); 312 return view; 313 } 314 } 315 } 316