Friday, November 28, 2014

Building a Widget to Silence a Phone

    One of the key parts of Android is the ability to customize your experience through components like widgets. In this tutorial I will go over creating a widget to silence a phone with one click, and in Lollipop (at least with the Nexus 4) it will place the phone into priority mode and silence the ringer, allowing the user to still get their notifications without a vibration. All code for this app can be found on GitHub.


    The first thing we're going to want to do is set up our manifest. Under the applications node, we'll add a receiver and service.

    <receiver android:name=".SilenceRingerWidget">
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        </intent-filter>
        <meta-data android:name="android.appwidget.provider"
            android:resource="@xml/silence_ringer_widget" />
    </receiver>

    <service android:name=".SilenceRingerService" />

    SilenceRingerService is responsible for the actual background work that our widget performs. The receiver is an extension of AppWidgetProvider, which is a special extension of BroadcastReceiver for app widgets. This allows it to be set to listen for APPWIDGET_UPDATE from the system. The meta-data provided is an appwidget-provider xml file that simply sets the layout for the widget, refresh rate and size.

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="0"
    android:initialLayout="@layout/widget_silence_ringer"/>

    The only other additions we need to make to our manifest is to set the permissions that our widget will need in order to silence the phone.

    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.WRITE_SETTINGS" />

    SilenceRingerWidget only has one method in this example, onUpdate, which is called when the widget is placed on the home screen and when the widget updates (which, in this case, won't happen since the updatePeriodMillis is set to 0 in silence_ringer_widget.xml). This method starts up the background service that builds the widget and silences the device.

@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
    context.startService(new Intent(context, SilenceRingerService.class));
}

    SilenceRingerService is where the bulk of this widget is handled. onStartCommand silences the phone, builds the remote view and then kills the service until the widget button is pressed again.

@Override
public int onStartCommand(Intent intent, int flags, int startId) {

    silencePhone();
    RemoteViews views = buildViews();
    updateWidget( views );

    stopSelf();
    return START_NOT_STICKY;
}

    silencePhone() uses the AudioManager to silence the device, and because of Lollipop we also include a thread that starts up after a second and does the same action a second time, because Lollipop first places the device into Priority mode without silencing the device, then silences the ringer after the second setRingerMode call.

private void silencePhone() {
    setPriorityAndSilence();
    new Thread( new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep( 1000 );
            } catch( InterruptedException e ) {

            }

            setPriorityAndSilence();

        }
    } ).run();
}

private void setPriorityAndSilence() {
    AudioManager audioManager;
    audioManager = (AudioManager) getBaseContext().getSystemService( Context.AUDIO_SERVICE );
    audioManager.setRingerMode( AudioManager.RINGER_MODE_SILENT );
}

    The last part of onStartCommand that we need to look at has to do with RemoteViews. We simply grab the button from our layout file and set a pending intent to it that will start up SilenceRingerService again, and associate it with our widget.

private RemoteViews buildViews() {
    RemoteViews views = new RemoteViews( getPackageName(), R.layout.widget_silence_ringer );
    PendingIntent silenceIntent = PendingIntent.getService( this, 0, new Intent( this, SilenceRingerService.class ), 0 );
    views.setOnClickPendingIntent( R.id.button_silence, silenceIntent );
    return views;
}

private void updateWidget( RemoteViews views ) {
    AppWidgetManager manager = AppWidgetManager.getInstance( this );
    ComponentName widget = new ComponentName( this, SilenceRingerWidget.class );
    manager.updateAppWidget( widget, views );
}

    Now we have a simple widget put together that can be expanded on to fit other situations that may come up for you as a developer. Enjoy!


Saturday, November 8, 2014

Building an Android Google Cast Sender App

This post was originally written for BinPress.

    With millions of Chromecasts sold and the Android TV ready to burst into living rooms, knowing how to build an app that supports casting should be in every Android developer's toolkit. This post will cover making an Android sender app to cast videos to a receiver app for displaying content on a television. All code for this example can be found on GitHub.

    For this tutorial, we'll use an unstyled receiver application created through the Google Cast Developer Console. While there is a generic default receiver application that can be used, this will provide an option to style the default receiver, or build your own if needed. Once the generic receiver application has been created, make note of the application ID, as we'll use that later. If you haven't already, you'll also want to register your casting device in order to run debug applications through Google Cast.

    Now, let's get to work on our Android application. The first thing that should be done is adding the libraries that we'll need to our build.gradle file.

compile 'com.android.support:appcompat-v7:20.0.0'
compile 'com.android.support:mediarouter-v7:19.0.+'
compile 'com.google.android.gms:play-services:6.1.11'

    Once build.gradle is all set, we can add the Internet permission and play services to AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET" />

<application
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/Theme.AppCompat" >

    <meta-data
        android:name="com.google.android.gms.version"
        android:value="@integer/google_play_services_version" />

 ...

    The next thing that we'll need to do is add the media routing button to our activity's menu file. This action bar button handles all of the state logic needed for hiding or displaying the casting button, as well as coloring it when the app has connected to a casting device.

<item android:id="@+id/media_route_menu_item"
    android:title="Chromecast"
    app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
    app:showAsAction="always"
    />

    For our last housekeeping item, we'll need to add some values to strings.xml, such as the video URL, receiver application ID and general UI strings. You'll need to replace app_id with your application ID, and video_url with a video that you would want to play. For this example I picked up a link from Archive.org for the George Romero classic Night of the Living Dead.

<string name="app_name">Chromecast</string>
<string name="video_url">xxxxx</string>
<string name="app_id">xxxxx</string>
<string name="play_video">Play Video</string>
<string name="pause_video">Pause Video</string>
<string name="resume_video">Resume Video</string>
<string name="video_title">Paul\'s Chromecast Video Stream</string>
<string name="content_type_mp4">video/mp4</string> 

    Now that all of the preparation work is done, we can move on to working with our casting activity - MainActivity.java. To start, I've added a set of declarations to the top of the class to keep track of our casting components and general state logic.

private Button mButton;

private MediaRouter mMediaRouter;
private MediaRouteSelector mMediaRouteSelector;
private MediaRouter.Callback mMediaRouterCallback;
private CastDevice mSelectedDevice;
private GoogleApiClient mApiClient;
private RemoteMediaPlayer mRemoteMediaPlayer;
private Cast.Listener mCastClientListener;
private boolean mWaitingForReconnect = false;
private boolean mApplicationStarted = false;
private boolean mVideoIsLoaded;
private boolean mIsPlaying;

    The UI for this activity consists of a single button that will start a video if the app is connected to a casting device, or can pause/resume a video once it has been started, and the media router button in the action bar. In onCreate we simply initialize the click listener for the main control button and the media router, selector and callbacks.

@Override
protected void onCreate( Bundle savedInstanceState ) {
    super.onCreate( savedInstanceState );
    setContentView( R.layout.activity_main );

    mButton = (Button) findViewById( R.id.button );
    mButton.setOnClickListener( new OnClickListener() {
        @Override
        public void onClick( View v ) {
           if( !mVideoIsLoaded )
               startVideo();
           else
               controlVideo();
        }
    });

    initMediaRouter();
}

private void initMediaRouter() {
    // Configure Cast device discovery
    mMediaRouter = MediaRouter.getInstance( getApplicationContext() );
    mMediaRouteSelector = new MediaRouteSelector.Builder()
            .addControlCategory( 
                CastMediaControlIntent.categoryForCast( getString( R.string.app_id ) ) )
            .build();
    mMediaRouterCallback = new MediaRouterCallback();
}

    Since the media router button exists in a menu file, we need to also override onCreateOptionsMenu and associate our selector with the MediaRouteActionProvider for the media router menu item.

@Override
public boolean onCreateOptionsMenu( Menu menu ) {
    super.onCreateOptionsMenu( menu );
    getMenuInflater().inflate( R.menu.main, menu );
    MenuItem mediaRouteMenuItem = menu.findItem( R.id.media_route_menu_item );
    MediaRouteActionProvider mediaRouteActionProvider = (MediaRouteActionProvider) MenuItemCompat.getActionProvider( mediaRouteMenuItem );
    mediaRouteActionProvider.setRouteSelector( mMediaRouteSelector );
    return true;
}

    We'll also want to associate the media router callbacks with our router button in onResume.

@Override
protected void onResume() {
    super.onResume();
    // Start media router discovery
    mMediaRouter.addCallback( mMediaRouteSelector, mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN );
}

    mMediaRouterCallback is an extension of MediaRouter.Callback, and provides methods for performing actions when a route is selected or unselected.

private class MediaRouterCallback extends MediaRouter.Callback {

    @Override
    public void onRouteSelected(MediaRouter router, RouteInfo info) {
        initCastClientListener();
        initRemoteMediaPlayer();

        mSelectedDevice = CastDevice.getFromBundle( info.getExtras() );

        launchReceiver();
    }

    @Override
    public void onRouteUnselected( MediaRouter router, RouteInfo info ) {
        teardown();
        mSelectedDevice = null;
        mButton.setText( getString( R.string.play_video ) );
        mVideoIsLoaded = false;
    }
}

    At this point your application should display the media router button in the action bar if you are on a network with a casting device (assuming you have stub methods in where necessary).


    It will also allow you to choose a casting device to interact with.


    As you probably noticed, MediaRouterCallback does a few different things in onRouteSelected. First it initializes mCastClientListener, which has methods for checking when an application status has changed, volume has changed or the client has disconnected from the receiver application (I'll go over the teardown method later).

private void initCastClientListener() {
    mCastClientListener = new Cast.Listener() {
        @Override
        public void onApplicationStatusChanged() {
        }

        @Override
        public void onVolumeChanged() {
        }

        @Override
        public void onApplicationDisconnected( int statusCode ) {
            teardown();
        }
    };
}

    Next the RemoteMediaPlayer is initialized. This object controls playback with the receiver, as well as provides a lot of useful state and status information.

    private void initRemoteMediaPlayer() {
        mRemoteMediaPlayer = new RemoteMediaPlayer();
        mRemoteMediaPlayer.setOnStatusUpdatedListener( new RemoteMediaPlayer.OnStatusUpdatedListener() {
            @Override
            public void onStatusUpdated() {
                MediaStatus mediaStatus = mRemoteMediaPlayer.getMediaStatus();
                mIsPlaying = mediaStatus.getPlayerState() == MediaStatus.PLAYER_STATE_PLAYING;
            }
        });

        mRemoteMediaPlayer.setOnMetadataUpdatedListener( new RemoteMediaPlayer.OnMetadataUpdatedListener() {
            @Override
            public void onMetadataUpdated() {
            }
        });
    }

    Next, the selected device is stored and the receiver application is launched by using Google Play Services

private void launchReceiver() {
    Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions
            .builder( mSelectedDevice, mCastClientListener );

    ConnectionCallbacks mConnectionCallbacks = new ConnectionCallbacks();
    ConnectionFailedListener mConnectionFailedListener = new ConnectionFailedListener();
    mApiClient = new GoogleApiClient.Builder( this )
            .addApi( Cast.API, apiOptionsBuilder.build() )
            .addConnectionCallbacks( mConnectionCallbacks )
            .addOnConnectionFailedListener( mConnectionFailedListener )
            .build();

    mApiClient.connect();
}

    ConnectionCallbacks is another inner class that extends GoogleApiClient.ConnectionCallbacks. When the API client has connected from launchReceiver, the Cast API is used to actually launch the receiver specified by the application ID that we received earlier from the Google Cast Developers Console

private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks {

@Override
public void onConnected( Bundle hint ) {
    if( mWaitingForReconnect ) {
        mWaitingForReconnect = false;
        reconnectChannels( hint );
    } else {
        try {
            Cast.CastApi.launchApplication( mApiClient, getString( R.string.app_id ), false )
                    .setResultCallback( 
                         new ResultCallback<Cast.ApplicationConnectionResult>() {
                        @Override
                        public void onResult(
                           Cast.ApplicationConnectionResult applicationConnectionResult) {
                            Status status = applicationConnectionResult.getStatus();
                            if( status.isSuccess() ) {
                                //Values that can be useful for storing/logic
                                ApplicationMetadata applicationMetadata = 
                                       applicationConnectionResult.getApplicationMetadata();
                                String sessionId = 
                                       applicationConnectionResult.getSessionId();
                                String applicationStatus = 
                                       applicationConnectionResult.getApplicationStatus();
                                boolean wasLaunched = 
                                       applicationConnectionResult.getWasLaunched();

                                mApplicationStarted = true;
                                reconnectChannels( null );
                            }
                        }
                    }
            );
        } catch ( Exception e ) {

        }
    }
}

@Override
public void onConnectionSuspended(int i) {
    mWaitingForReconnect = true;
}

    At this point the application will start up our generic receiver application on the television.


    In onCreate we added a method call to startVideo. As you may have guessed, this method is used to start video playback on the receiver if it is connected. This method is also where we could start a local player if the app is not connected to a receiver.

private void startVideo() {
    MediaMetadata mediaMetadata = new MediaMetadata( MediaMetadata.MEDIA_TYPE_MOVIE );
    mediaMetadata.putString( MediaMetadata.KEY_TITLE, getString( R.string.video_title ) );

    MediaInfo mediaInfo = new MediaInfo.Builder( getString( R.string.video_url ) )
            .setContentType( getString( R.string.content_type_mp4 ) )
            .setStreamType( MediaInfo.STREAM_TYPE_BUFFERED )
            .setMetadata( mediaMetadata )
            .build();
    try {
        mRemoteMediaPlayer.load( mApiClient, mediaInfo, true )
                .setResultCallback( new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
                    @Override
                    public void onResult( RemoteMediaPlayer.MediaChannelResult mediaChannelResult ) {
                        if( mediaChannelResult.getStatus().isSuccess() ) {
                            mVideoIsLoaded = true;
                            mButton.setText( getString( R.string.pause_video ) );
                        }
                    }
                } );
    } catch( Exception e ) {
    }
} 




    When the video is casting to the receiver, the main UI button switches functionality to simply pausing or resuming the video by sending messages over the RemoteMediaControl

private void controlVideo() {
    if( mRemoteMediaPlayer == null || !mVideoIsLoaded )
        return;

    if( mIsPlaying ) {
        mRemoteMediaPlayer.pause( mApiClient );
        mButton.setText( getString( R.string.resume_video ) );
    } else {
        mRemoteMediaPlayer.play( mApiClient );
        mButton.setText( getString( R.string.pause_video ) );
    }
}



    The last few methods to cover generally have to do with some cleanup to keep the app from crashing and to properly close a connection to the receiver. When the connection fails or the app is no longer running, we will call the teardown method.

private void reconnectChannels( Bundle hint ) {
    if( ( hint != null ) && hint.getBoolean( Cast.EXTRA_APP_NO_LONGER_RUNNING ) ) {
        //Log.e( TAG, "App is no longer running" );
        teardown();
    } else {
        try {
            Cast.CastApi.setMessageReceivedCallbacks( mApiClient, mRemoteMediaPlayer.getNamespace(), mRemoteMediaPlayer );
        } catch( IOException e ) {
            //Log.e( TAG, "Exception while creating media channel ", e );
        } catch( NullPointerException e ) {
            //Log.e( TAG, "Something wasn't reinitialized for reconnectChannels" );
        }
    }
}

private class ConnectionFailedListener implements GoogleApiClient.OnConnectionFailedListener {
    @Override
    public void onConnectionFailed( ConnectionResult connectionResult ) {
        teardown();
    }
}

    ...where teardown simply closes the casting connection and disconnects from Play Services.

private void teardown() {
    if( mApiClient != null ) {
        if( mApplicationStarted ) {
            try {
                Cast.CastApi.stopApplication( mApiClient );
                if( mRemoteMediaPlayer != null ) {
                    Cast.CastApi.removeMessageReceivedCallbacks( mApiClient, mRemoteMediaPlayer.getNamespace() );
                    mRemoteMediaPlayer = null;
                }
            } catch( IOException e ) {
                //Log.e( TAG, "Exception while removing application " + e );
            }
            mApplicationStarted = false;
        }
        if( mApiClient.isConnected() )
            mApiClient.disconnect();
        mApiClient = null;
    }
    mSelectedDevice = null;
    mVideoIsLoaded = false;
}


    We also have to remember to remove callbacks from the MediaRouter that we added during onResume.

@Override
protected void onPause() {
    if ( isFinishing() ) {
        // End media router discovery
        mMediaRouter.removeCallback( mMediaRouterCallback );
    }
    super.onPause();
}

    And with that, we have the base functionality for a Google Cast capable Android sender app. If the source code on GitHub doesn't make sense, or you have other questions, please comment below!