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