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!