com.qiusheng.cast.CastActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.qiusheng.cast.CastActivity.java

Source

/*
 * Copyright (C) 2013 Google Inc. All Rights Reserved. 
 *
 * 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.qiusheng.cast;

import java.io.IOException;
import java.util.ArrayList;

import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v7.app.MediaRouteButton;
import android.support.v7.media.MediaRouteSelector;
import android.support.v7.media.MediaRouter;
import android.support.v7.media.MediaRouter.RouteInfo;
import android.text.Html;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;

import com.google.cast.ApplicationChannel;
import com.google.cast.ApplicationMetadata;
import com.google.cast.ApplicationSession;
import com.google.cast.CastContext;
import com.google.cast.CastDevice;
import com.google.cast.ContentMetadata;
import com.google.cast.MediaProtocolCommand;
import com.google.cast.MediaProtocolMessageStream;
import com.google.cast.MediaRouteAdapter;
import com.google.cast.MediaRouteHelper;
import com.google.cast.MediaRouteStateChangeListener;
import com.google.cast.SessionError;
import com.qiusheng.cast.mediaroutedialog.SampleMediaRouteDialogFactory;

/***
 * An activity that plays a chosen sample video on a Cast device and exposes
 * playback and volume controls in the UI.
 */
public class CastActivity extends FragmentActivity implements MediaRouteAdapter {

    private static final String TAG = CastActivity.class.getSimpleName();

    public static final boolean ENABLE_LOGV = true;

    protected static final double MAX_VOLUME_LEVEL = 20;
    private static final double VOLUME_INCREMENT = 0.05;
    private static final int SEEK_FORWARD = 1;
    private static final int SEEK_BACK = 2;
    private static final int SEEK_INCREMENT = 10;

    private boolean mPlayButtonShowsPlay = false;
    private boolean mVideoIsStopped = false;

    private CastContext mCastContext = null;
    private static CastDevice mSelectedDevice;
    private CastMedia mMedia;
    private ContentMetadata mMetaData;
    private ApplicationSession mSession;
    private MediaProtocolMessageStream mMessageStream;
    private MediaRouteButton mMediaRouteButton;
    private MediaRouter mMediaRouter;
    private MediaRouteSelector mMediaRouteSelector;
    private MediaRouter.Callback mMediaRouterCallback;
    private MediaProtocolCommand mStatus;

    private ImageButton mPlayPauseButton;
    private ImageButton mStopButton;
    private TextView mStatusText;
    private TextView mCurrentlyPlaying;
    private String mCurrentItemId;
    private RouteInfo mCurrentRoute;

    private SampleMediaRouteDialogFactory mDialogFactory;

    private Thread myThread;

    private ArrayList<String> urls = new ArrayList<String>();
    private int nextUrl = 0;
    private CastActivity castActivity = this;
    private String title = "";

    /**
     * Initializes MediaRouter information and prepares for Cast device
     * detection upon creating this activity.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        logVIfEnabled(TAG, "onCreate called");
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_cast);

        mCastContext = new CastContext(getApplicationContext());
        mMedia = new CastMedia(null, null);
        mMetaData = new ContentMetadata();

        mDialogFactory = new SampleMediaRouteDialogFactory();

        MediaRouteHelper.registerMinimalMediaRouteProvider(mCastContext, this);
        mMediaRouter = MediaRouter.getInstance(getApplicationContext());
        mMediaRouteSelector = MediaRouteHelper.buildMediaRouteSelector(MediaRouteHelper.CATEGORY_CAST);

        mMediaRouteButton = (MediaRouteButton) findViewById(R.id.media_route_button);
        mMediaRouteButton.setRouteSelector(mMediaRouteSelector);
        mMediaRouteButton.setDialogFactory(mDialogFactory);
        mMediaRouterCallback = new MyMediaRouterCallback();

        mStatusText = (TextView) findViewById(R.id.play_status_text);
        mCurrentlyPlaying = (TextView) findViewById(R.id.currently_playing);
        mCurrentlyPlaying.setText(getString(R.string.tap_to_select));

        mPlayPauseButton = (ImageButton) findViewById(R.id.play_pause_button);
        mStopButton = (ImageButton) findViewById(R.id.stop_button);
        initButtons();

        myThread = null;
        Runnable runnable = new StatusRunner();
        myThread = new Thread(runnable);
        logVIfEnabled(TAG, "Starting statusRunner thread");
        myThread.start();

        Intent intent = getIntent();
        String url = intent.getStringExtra("MEDIA_URL");
        urls = intent.getStringArrayListExtra("MEDIA_URL_LIST");
        if (urls == null || urls.size() == 0) {
            urls = new ArrayList<String>();
            urls.add(url);
        }
        title = intent.getStringExtra("MEDIA_TITLE");

        if (title == null || title.isEmpty())
            title = urls.get(0);

        this.mediaSelected(new CastMedia(title, urls.get(0)));
        nextUrl++;
    }

    /**
     * Initializes all buttons by adding user controls and listeners.
     */
    public void initButtons() {
        mPlayPauseButton.setEnabled(false);
        mPlayPauseButton.setImageResource(R.drawable.pause_button);
        mPlayPauseButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onPlayClicked(!mPlayButtonShowsPlay);
            }
        });
        mStopButton.setEnabled(false);
        mStopButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onStopClicked();
            }
        });
        mCurrentlyPlaying.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // go back to http activity
                finish();
            }
        });
    }

    /**
     * Skips forward or backward by some fixed increment in the currently
     * playing media.
     * 
     * @param direction
     *            an integer corresponding to either SEEK_FORWARD or SEEK_BACK
     */
    public void onSeekClicked(int direction) {
        try {
            if (mMessageStream != null) {
                double cPosition = mMessageStream.getStreamPosition();
                if (direction == SEEK_FORWARD) {
                    mMessageStream.playFrom(cPosition + SEEK_INCREMENT);
                } else if (direction == SEEK_BACK) {
                    mMessageStream.playFrom(cPosition - SEEK_INCREMENT);
                } else {
                    Log.e(TAG, "onSeekClicked was not FWD or BACK");
                }
            } else {
                Log.e(TAG, "onSeekClicked - mMPMS==null");
            }
        } catch (IOException e) {
            Log.e(TAG, "Failed to send pause comamnd.");
        }
    }

    /**
     * Handles stopping the currently playing media upon the stop button being
     * pressed.
     */
    public void onStopClicked() {
        try {
            if (mMessageStream != null) {
                mMessageStream.stop();
                mVideoIsStopped = !mVideoIsStopped;
                mPlayPauseButton.setImageResource(R.drawable.play_button);
                mPlayButtonShowsPlay = true;
            } else {
                Log.e(TAG, "onStopClicked - mMPMS==null");
            }
        } catch (IOException e) {
            Log.e(TAG, "Failed to send pause comamnd.");
        }
    }

    /**
     * Mutes the currently playing media when the mute button is pressed.
     */
    public void onMuteClicked() {
        try {
            if (mMessageStream != null) {
                if (mMessageStream.isMuted()) {
                    mMessageStream.setMuted(false);
                } else {
                    mMessageStream.setMuted(true);
                }
            } else {
                Log.e(TAG, "onMutedClicked - mMPMS==null");
            }
        } catch (IOException e) {
            Log.e(TAG, "Failed to send pause comamnd.");
        }
    }

    /**
     * Plays or pauses the currently loaded media, depending on the current
     * state of the <code>
     * mPlayPauseButton</code>.
     * 
     * @param playState
     *            indicates that Play was clicked if true, and Pause was clicked
     *            if false
     */
    public void onPlayClicked(boolean playState) {
        if (playState) {
            try {
                if (mMessageStream != null) {
                    mMessageStream.stop();
                } else {
                    Log.e(TAG, "onClick-Play - mMPMS==null");
                }
            } catch (IOException e) {
                Log.e(TAG, "Failed to send stop comamnd.");
            }
            mPlayPauseButton.setImageResource(R.drawable.play_button);
        } else {
            try {
                if (mMessageStream != null) {
                    if (mVideoIsStopped) {
                        mMessageStream.play();
                        mVideoIsStopped = !mVideoIsStopped;
                    } else {
                        mMessageStream.resume();
                    }
                } else {
                    Log.e(TAG, "onClick-Play - mMPMS==null");
                }
            } catch (IOException e) {
                Log.e(TAG, "Failed to send play/resume comamnd.");
            }
            mPlayPauseButton.setImageResource(R.drawable.pause_button);
        }
        mPlayButtonShowsPlay = !mPlayButtonShowsPlay;
    }

    @Override
    public void onDeviceAvailable(CastDevice device, String myString, MediaRouteStateChangeListener listener) {
        mSelectedDevice = device;
        logVIfEnabled(TAG, "Available device found: " + myString);
        openSession();
    }

    @Override
    public void onSetVolume(double volume) {
        try {
            mMessageStream.setVolume(volume);
        } catch (IllegalStateException e) {
            Log.e(TAG, "Problem sending Set Volume", e);
        } catch (IOException e) {
            Log.e(TAG, "Problem sending Set Volume", e);
        }
    }

    @Override
    public void onUpdateVolume(double volumeChange) {
        try {
            if ((mCurrentItemId != null) && (mCurrentRoute != null)) {
                mCurrentRoute.requestUpdateVolume((int) (volumeChange * MAX_VOLUME_LEVEL));
            }
        } catch (IllegalStateException e) {
            Log.e(TAG, "Problem sending Update Volume", e);
        }
    }

    /**
     * Processes volume up and volume down actions upon receiving them as key
     * events.
     */
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        int action = event.getAction();
        int keyCode = event.getKeyCode();
        switch (keyCode) {
        case KeyEvent.KEYCODE_VOLUME_UP:
            if (action == KeyEvent.ACTION_DOWN) {
                double currentVolume;
                if (mMessageStream != null) {
                    currentVolume = mMessageStream.getVolume();
                    logVIfEnabled(TAG, "Volume up from " + currentVolume);
                    if (currentVolume < 1.0) {
                        logVIfEnabled(TAG, "New volume: " + (currentVolume + VOLUME_INCREMENT));
                        onSetVolume(currentVolume + VOLUME_INCREMENT);
                    }
                } else {
                    Log.e(TAG, "dispatchKeyEvent - volume up - mMessageStream==null");
                }
            }

            return true;
        case KeyEvent.KEYCODE_VOLUME_DOWN:
            if (action == KeyEvent.ACTION_DOWN) {
                double currentVolume;
                if (mMessageStream != null) {
                    currentVolume = mMessageStream.getVolume();
                    logVIfEnabled(TAG, "Volume down from: " + currentVolume);
                    if (currentVolume > 0.0) {
                        logVIfEnabled(TAG, "New volume: " + (currentVolume - VOLUME_INCREMENT));
                        onSetVolume(currentVolume - VOLUME_INCREMENT);
                    }
                } else {
                    Log.e(TAG, "dispatchKeyEvent - volume down - mMessageStream==null");
                }
            }
            return true;
        default:
            return super.dispatchKeyEvent(event);
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback,
                MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
        logVIfEnabled(TAG, "onStart called and callback added");
    }

    /**
     * Closes a running session upon destruction of this Activity.
     */
    @Override
    protected void onStop() {
        mMediaRouter.removeCallback(mMediaRouterCallback);
        super.onStop();
        logVIfEnabled(TAG, "onStop called and callback removed");
    }

    @Override
    protected void onDestroy() {
        logVIfEnabled(TAG, "onDestroy called, ending session if session exists");
        if (mSession != null) {
            try {
                if (!mSession.hasStopped()) {
                    mSession.endSession();
                }
            } catch (IOException e) {
                Log.e(TAG, "Failed to end session.");
            }
        }
        mSession = null;

        if (myThread.isAlive())
            myThread.interrupt();

        super.onDestroy();
    }

    /**
     * A callback class which listens for route select or unselect events and
     * processes devices and sessions accordingly.
     */
    private class MyMediaRouterCallback extends MediaRouter.Callback {
        @Override
        public void onRouteSelected(MediaRouter router, RouteInfo route) {
            MediaRouteHelper.requestCastDeviceForRoute(route);
        }

        @Override
        public void onRouteUnselected(MediaRouter router, RouteInfo route) {
            try {
                if (mSession != null) {
                    logVIfEnabled(TAG, "Ending session and stopping application");
                    mSession.setStopApplicationWhenEnding(true);
                    mSession.endSession();
                } else {
                    Log.e(TAG, "onRouteUnselected: mSession is null");
                }
            } catch (IllegalStateException e) {
                Log.e(TAG, "onRouteUnselected:");
                e.printStackTrace();
            } catch (IOException e) {
                Log.e(TAG, "onRouteUnselected:");
                e.printStackTrace();
            }
            mMessageStream = null;
            mSelectedDevice = null;
        }
    }

    /**
     * Starts a new video playback session with the current CastContext and
     * selected device.
     */
    private void openSession() {
        logVIfEnabled(TAG, "openSession");
        mSession = new ApplicationSession(mCastContext, mSelectedDevice);

        // TODO: The below lines allow you to specify either that your
        // application uses the default
        // implementations of the Notification and Lock Screens, or that you
        // will be using your own.
        int flags = 0;

        // Comment out the below line if you are not writing your own
        // Notification Screen.
        // flags |= ApplicationSession.FLAG_DISABLE_NOTIFICATION;

        // Comment out the below line if you are not writing your own Lock
        // Screen.
        // flags |= ApplicationSession.FLAG_DISABLE_LOCK_SCREEN_REMOTE_CONTROL;
        mSession.setApplicationOptions(flags);

        logVIfEnabled(TAG, "Beginning session with context: " + mCastContext);
        logVIfEnabled(TAG, "The session to begin: " + mSession);
        mSession.setListener(new com.google.cast.ApplicationSession.Listener() {

            @Override
            public void onSessionStarted(ApplicationMetadata appMetadata) {
                logVIfEnabled(TAG, "Getting channel after session start");
                ApplicationChannel channel = mSession.getChannel();
                if (channel == null) {
                    Log.e(TAG, "channel = null");
                    return;
                }
                logVIfEnabled(TAG, "Creating and attaching Message Stream");
                mMessageStream = new MediaProtocolMessageStream();
                channel.attachMessageStream(mMessageStream);

                if (mMessageStream.getPlayerState() == null) {
                    if (mMedia != null) {
                        loadMedia();
                    }
                } else {
                    logVIfEnabled(TAG, "Found player already running; updating status");
                    updateStatus();
                }
            }

            @Override
            public void onSessionStartFailed(SessionError error) {
                Log.e(TAG, "onStartFailed " + error);
            }

            @Override
            public void onSessionEnded(SessionError error) {
                Log.i(TAG, "onEnded " + error);
            }
        });

        mPlayPauseButton.setEnabled(true);
        mStopButton.setEnabled(true);
        try {
            // TODO: To run your own copy of the receiver, you will need to set
            // app_name in
            // /res/strings.xml to your own appID, and then upload the provided
            // receiver
            logVIfEnabled(TAG, "Starting session with app id " + getString(R.string.app_id));

            // TODO: To run your own copy of the receiver, you will need to set app_name in 
            // /res/strings.xml to your own appID, and then upload the provided receiver 
            // to the url that you whitelisted for your app.
            // The current value of app_name is "YOUR_APP_ID_HERE".
            mSession.startSession(getString(R.string.app_id));
        } catch (IOException e) {
            Log.e(TAG, "Failed to open session", e);
        }
    }

    /**
     * Loads the stored media object and casts it to the currently selected
     * device.
     */
    protected void loadMedia() {
        logVIfEnabled(TAG, "Loading selected media on device");
        mMetaData.setTitle(mMedia.getTitle());
        try {
            MediaProtocolCommand cmd = mMessageStream.loadMedia(mMedia.getUrl(), mMetaData, true);
            cmd.setListener(new MediaProtocolCommand.Listener() {

                @Override
                public void onCompleted(MediaProtocolCommand mPCommand) {
                    logVIfEnabled(TAG, "Load completed - starting playback");
                    mPlayPauseButton.setImageResource(R.drawable.pause_button);
                    mPlayButtonShowsPlay = false;
                    onSetVolume(0.9);
                }

                @Override
                public void onCancelled(MediaProtocolCommand mPCommand) {
                    logVIfEnabled(TAG, "Load cancelled");
                }
            });

        } catch (IllegalStateException e) {
            Log.e(TAG, "Problem occurred with MediaProtocolCommand during loading", e);
        } catch (IOException e) {
            Log.e(TAG, "Problem opening MediaProtocolCommand during loading", e);
        }
    }

    /**
     * Stores and attempts to load the passed piece of media.
     */
    protected void mediaSelected(CastMedia media) {
        logVIfEnabled(TAG, "mediaSelected");
        this.mMedia = media;
        updateCurrentlyPlaying();
        if (mSelectedDevice != null) {
            openSession();
        }
        if (mMessageStream != null) {
            loadMedia();
        }
    }

    /**
     * Updates the status of the currently playing video in the dedicated
     * message view.
     */
    public void updateStatus() {
        // logVIfEnabled(TAG, "updateStatus");
        this.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                try {
                    setMediaRouteButtonVisible();
                    updateCurrentlyPlaying();

                    if (mMessageStream != null) {
                        mStatus = mMessageStream.requestStatus();

                        String currentStatus = "Player State: " + mMessageStream.getPlayerState() + "\n";
                        currentStatus += "Device " + mSelectedDevice.getFriendlyName() + "\n";
                        currentStatus += "Title " + mMessageStream.getTitle() + "\n";
                        currentStatus += "Current Position: " + mMessageStream.getStreamPosition() + "\n";
                        currentStatus += "Duration: " + mMessageStream.getStreamDuration() + "\n";
                        currentStatus += "Volume set at: " + (mMessageStream.getVolume() * 100) + "%\n";
                        currentStatus += "requestStatus: " + mStatus.getType() + "\n";
                        mStatusText.setText(currentStatus);

                        // continue playing next url
                        if (nextUrl < urls.size()
                                && mMessageStream.getStreamPosition() == mMessageStream.getStreamDuration()) {
                            nextUrl++;
                            castActivity.mediaSelected(new CastMedia(title + "part " + nextUrl, urls.get(nextUrl)));

                            Toast.makeText(castActivity.getApplicationContext(), "Auto play next part",
                                    Toast.LENGTH_LONG).show();
                        }
                    } else {
                        mStatusText.setText(getResources().getString(R.string.tap_icon));
                    }
                } catch (Exception e) {
                    Log.e(TAG, "Status request failed: " + e);
                }
            }
        });
    }

    /**
     * Sets the Cast Device Selection button to visible or not, depending on the
     * availability of devices.
     */
    protected final void setMediaRouteButtonVisible() {
        mMediaRouteButton
                .setVisibility(mMediaRouter.isRouteAvailable(mMediaRouteSelector, 0) ? View.VISIBLE : View.GONE);
    }

    /**
     * Updates a view with the title of the currently playing media.
     */
    protected void updateCurrentlyPlaying() {
        // logVIfEnabled(TAG, "updateCurrentlyPlaying");
        String playing = "";
        if (mMedia.getTitle() != null) {
            playing = "Media Selected: " + mMedia.getTitle();
            if (mMessageStream != null) {
                String colorString = "<br><font color=#0066FF>";
                colorString += "Casting to " + mSelectedDevice.getFriendlyName();
                colorString += "</font>";
                playing += colorString;
            }
            mCurrentlyPlaying.setText(Html.fromHtml(playing));
        } else {
            mCurrentlyPlaying.setText("No media selected");
        }
    }

    /**
     * A Runnable class that updates a view to display status for the currently
     * playing media.
     */
    private class StatusRunner implements Runnable {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    updateStatus();
                    Thread.sleep(3000);
                } catch (Exception e) {
                    Log.e(TAG, "Thread interrupted: " + e);
                }
            }
        }
    }

    /**
     * Logs in verbose mode with the given tag and message, if the LOCAL_LOGV
     * tag is set.
     */
    private void logVIfEnabled(String tag, String message) {
        if (ENABLE_LOGV) {
            Log.v(tag, message);
        }
    }
}