Thursday, May 16, 2013

API for Fetching the Google Voice Configuration on Android

Recent version of the Google Voice app on Android include a service which returns a Bundle of configuration values.  Currently the only value provided in the bundle is the subscriber number (the Google Voice phone number).

We have a need to programatically determine the user's Google Voice phone number from our own app.  Here's how it can be done...

First, include the Google Voice FETCH_CONFIGURATION permission in your app's AndroidManifest.xml file:

<uses-permission android:name="com.google.android.apps.googlevoice.permission.FETCH_CONFIGURATION"/>

Next, include the following three files in your src directory:

src/com/google/android/apps/googlevoice/IGoogleVoiceConfiguration.aidl

package com.google.android.apps.googlevoice;

interface IGoogleVoiceConfiguration {
    Bundle getConfiguration();
} 

src/com/google/android/apps/googlevoice/GoogleVoiceConfigurationAPI.java

package com.google.android.apps.googlevoice;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;

import android.util.Log;

/**
 * API for fetching the Google Voice configuration. It currently only
 * provides one value, "subscriberNumber".
 */
public class GoogleVoiceConfigurationAPI implements ServiceConnection {

    private String logTag = "GoogleVoiceConfigAPI";

    public GoogleVoiceConfigurationAPI() {}

    public GoogleVoiceConfigurationAPI( String logTag) {
        this.logTag = logTag;
    }

    /**
     * Key to use for extracting the subscriber number from the configuration bundle.
     */
    public static final String KEY_SUBSCRIBER_NUMBER = "subscriberNumber";

    private static final String GOOGLE_VOICE_CONFIGURATION_SERVICE_ACTION =
        "com.google.android.apps.googlevoice.IGoogleVoiceConfiguration";

    private Bundle configuration = null;

    Context context = null;
    GoogleVoiceConfigurationListener listener = null;

    /**
     * Fetch the configuration and invoke the listener callback with the configuration bundle,
     * if available.
     * @param context context required to make service calls to Google Voice
     * @param listener listener to receive the configuration bundle - the listener's
     * onFetchGoogleVoiceConfiguration() will be called with the configuration info if this
     * method returns true.
     * @return true if the GoogleVoiceConfiguration service can be bound, otherwise false.
     * If true, you can expect the listener to be called, otherwise it won't be invoked.
     */
    public boolean fetchConfiguration(Context context, GoogleVoiceConfigurationListener listener) {
        if (context == null || listener == null)
            throw new IllegalArgumentException("null parameter values not allowed");

        if (configuration != null) {
            try {
                listener.onFetchGoogleVoiceConfiguration(configuration);
            } catch (Exception x) {
                Log.e(logTag, x.getMessage(), x);
            }
            return true;
        }

        this.context = context;
        this.listener = listener;

        Intent i = new Intent(GOOGLE_VOICE_CONFIGURATION_SERVICE_ACTION);
        return context.bindService(i, this, Context.BIND_AUTO_CREATE);
    }

    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
        try {
            IGoogleVoiceConfiguration iGoogleVoiceConfiguration = IGoogleVoiceConfiguration.Stub.asInterface(iBinder);
            configuration = iGoogleVoiceConfiguration.getConfiguration();
            if (listener != null) listener.onFetchGoogleVoiceConfiguration(configuration);
        } catch (Exception x) {
            Log.e(logTag, x.getMessage(), x);
        } finally {
            context.unbindService(this);
        }
    }

    public void onServiceDisconnected(ComponentName componentName) {
    }
}

src/com/google/android/apps/googlevoic/GoogleVoiceConfigurationListener.java

package com.google.android.apps.googlevoice;

import android.os.Bundle;

/**
 * Listener interface for receiving the google voice configuration bundle.
 */
public interface GoogleVoiceConfigurationListener {

    public void onFetchGoogleVoiceConfiguration(Bundle configuration);
}


Finally, you can call the API from your own app:
package com.example.googlevoice.config;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.google.android.apps.googlevoice.GoogleVoiceConfigurationAPI;
import com.google.android.apps.googlevoice.GoogleVoiceConfigurationListener;

public class GoogleVoiceConfigActivity extends Activity
{
    static final String TAG = "GoogleVoiceConfig";

    GoogleVoiceConfigurationAPI googleVoiceConfigurationAPI = new GoogleVoiceConfigurationAPI(TAG);

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        Button fetchItButton = (Button)findViewById(R.id.fetchConfigButton);

        fetchItButton.setOnClickListener( new View.OnClickListener() {
            public void onClick(View view) {
                TextView numberTextView = (TextView)findViewById(R.id.numberTextView);
                try {
                    if (!googleVoiceConfigurationAPI.fetchConfiguration(GoogleVoiceConfigActivity.this, listener))
                    {
                        numberTextView.setText(
                            "Configuration unavailable: Google Voice is not installed or not configured");
                    }
                } catch (SecurityException x) {
                    // This app is missing the following permission in its AndroidManifest.xml file:
                    // <uses-permission android:name="com.google.android.apps.googlevoice.permission.FETCH_CONFIGURATION">
                    Log.e(TAG, x.getMessage(), x);
                    numberTextView.setText( "Permission denied: this app's AndroidManifest.xml is missing permission"+
                        " com.google.android.apps.googlevoice.permission.FETCH_CONFIGURATION");
                } catch (Exception x) {
                    Log.e(TAG, x.getMessage(), x);
                    numberTextView.setText(x.getMessage());
                }
            }
        });
    }

    GoogleVoiceConfigurationListener listener = new GoogleVoiceConfigurationListener() {
        public void onFetchGoogleVoiceConfiguration(Bundle configuration)
        {
            String subscriberNumber = configuration.getString( GoogleVoiceConfigurationAPI.KEY_SUBSCRIBER_NUMBER);
            TextView numberTextView = (TextView)findViewById(R.id.numberTextView);
            numberTextView.setText("Subscriber number: " + subscriberNumber);
        }
    };
}
Full source can be downloaded from: this link

Wednesday, January 21, 2009

Casify MediaWiki with phpCAS

This recipe attempts to cover the steps necessary to secure MediaWiki, the popular wiki originally developed for Wikipedia, using Central Authentication Services (CAS). CAS provides single-sign-on authentication -- log into one of your applications and be automatically authenticated with the rest.

Let me start by pointing out that these instructions only cover getting the user authenticated. It does not include information related to authorization -- i.e., role-based access, etc. After following these steps the Wiki will only be readable to users that have successfully logged in, and users that can't log in won't be able to read the wiki. See LocalSettings.php, below, on some options to make it readable to anonymous users. If you need role-based access this is probably a good starting point anyway.

These steps were developed using information from the following sources:

The case.edu site contains a page with general instructions on how to implement external authentication for MediaWiki. Its not CAS specific, but provides a good basis for integrating the CAS client, phpCAS.

I'll assume you already have MediaWiki up and running, but as a bit of background here are some details on my setup. I'm running Debian "Lenny" with PHP5 and MySQL-Server-5.0. I was able to get MediaWiki installed using 'apt-get install mediawiki. With this method the MediaWiki install location is /var/lib/mediawiki. On other distributions, or if installed manually, the location is likely different.

Copies of all the files mentioned below are contained in a zip archive available here.

Step 1, Install phpCAS.


Take a close look at phpCAS requirements and decide which extra php packages should be installed. I decided to install the following additional debian packages as a result.

php5-curl, php5-gd, php5-mysql, php-db, php-pear, php-xml

Then follow the instructions here for installing phpCAS on your system.

Step 2, Create the CasAuthentication extension for MediaWiki


In this step we take the code from step 2.3 in the case.edu instructions and create CasAuthentication.php in the MediaWiki extensions folder. Change the class name from MyAuthPlugin to CasAuthPlugin, but don't make any other changes now.

Take a look at the initUser() function -- you will eventually want to update the code here to provide the correct name and email for the user. CAS only provides us with the user login name, or 'NetID' in CAS terms, so its here that you would have to look up the user information in a database or via LDAP, etc. I leave this as an exercise to the reader since its very site-specific.

Step 3, Create CAS login scripts


In the MediaWiki installation directory (/var/lib/mediawiki on debian) create a subdirectory named 'login'. Copy cas1.php and cas2.php into this directory. The file cas1.php is a slightly modified version of the code listed in step 2.4.1 of the case.edu instructions. I was unable to store the Http-Referrer in the session and have it persist until the end of the login sequence, so my version passes the referrer as a request parameter instead. The file cas2.php is a combination of the phpCAS simple client demo and the code listing from case.edu's step 2.4.2.

Step 4, Enable URL Rewrites


URL re-writing is used to invoke the cas php scripts instead of the default MediaWiki login page. The instructions in this recipe assume that the base URI of the wiki has been configured to /wiki -- i.e., that you get to it using http://localhost/wiki. If not the rewrite rules will have to be adjusted accordingly. On my debian system, the Apache config directives for MediaWiki are in /etc/apache2/conf.d/mediawiki.conf. Add the following rewrite rules in the <Directory> for the MediaWiki installation location (e.g., /var/lib/mediawiki):

RewriteEngine on

RewriteCond %{REQUEST_URI} ^/wiki/index.php$
RewriteCond %{QUERY_STRING} ^title=Special:Userlogin
RewriteCond %{REQUEST_METHOD} ^GET$
RewriteRule ^(.*)$ /wiki/login/cas1.php [R,L]

RewriteCond %{REQUEST_URI} ^/wiki/index.php$
RewriteCond %{QUERY_STRING} ^title=Special:Userlogout
RewriteCond %{REQUEST_METHOD} ^GET$
RewriteRule ^(.*)$ /wiki/login/cas2.php?logout [R,L]

Note that the second rule causes the logout link in MediWiki to invoke CAS logout. This may or may not be what you want.

Step 5, Configure LocalSettings.php


Add the following lines at the end of MediaWiki's LocalSettings.php file. Change the casServerHostname, casServerPort, casServiceUri and wgLoginFormKey values.


### BEGIN CAS CONFIGURATION ####

require_once "$IP/extensions/CasAuthentication.php";

$wgAuth = new CasAuthPlugin();

// CAS server hostname
$casServerHostname = 'cas.example.com';

// CAS server port. This is usually 443
$casServerPort = 443;

// This is the CAS web-application context URI
$casServiceURI = '/cas';

$wgLoginFormKey = "dC989kw6j0E41HN4I24E"; // Random key, change this

# AUthentication changes
$wgGroupPermissions ['*']['read']=false; // True here makes the wiki readable by anonymous users
$wgGroupPermissions ['*']['edit']=false;
$wgGroupPermissions ['*']['createaccount']=false;
$wgWhitelistRead = array("Special:Userlogin");

### END CAS CONFIGURATION ####


Restart Apache and if everything has been configured correctly you will be automatically redirected to the CAS page for MediaWiki logins, and upon success you should be redirected back to the referring URL.

Ken