Hide

PhotoHunt: Android

PhotoHunt is a daily photo scavenger hunt written in Java for Android that demonstrates Google+ Sign-In and the Google+ APIs by building a fun, social game based around taking photos. This article describes the application, and is designed to serve as both a guide to PhotoHunt on Android, and also as an example of how integrating Google+ into your Android app might look. You can also see PhotoHunt client implementations for iOS and the web such as the front-end client in the Java App Engine version of PhotoHunt. The back-end of the application is a set of web services that are implemented in several popular languages.

Browsing the source code

If you would like to browse the source code before digging in too deeply, simply have a look at PhotoHunt's Android client repository in GitHub.

Requirements

The PhotoHunt Android client has the following requirements for running the sample:

  • A PhotoHunt web service configured and running for the clients to send requests.
  • Your PhotoHunt clients and server must define their OAuth 2.0 client settings within the same Google Developers Console project for all features to work correctly.
  • A physical device to use for developing and testing because Google Play services can only be installed on an emulator with an AVD that runs Google APIs platform based on Android 4.2.2 or higher.
  • The latest version of the Android SDK, including the SDK Tools component. The SDK is available from the Android SDK Manager.
  • Your project to compile against Android 2.3 (Gingerbread) or higher.
  • Eclipse configured to use Java 1.6
  • The Google Play Services SDK:
    1. Launch Eclipse and select Window > Android SDK Manager or run android from the command line.
    2. Scroll to the bottom of the package list and select Extras > Google Play services. The package is downloaded to your computer and installed in your SDK environment at <android-sdk-folder>/extras/google/google_play_services.

Setting up PhotoHunt

The PhotoHunt app demonstrates using the Google+ Sign-In button, using SDK methods to list moments and profile information, as well as integrating with a back-end service to make requests to other Google APIs. To run a PhotoHunt client you must install a PhotoHunt server, which will provide the storage and API for your client.

In addition, the PhotoHunt Android client makes use of the ActionBarSherlock library.

Step 1: Enable the Google+ API

To authenticate and communicate with the Google+ APIs, you must create a Google Developers Console project, enable the Google+ API, create an OAuth 2.0 Client ID, and register your digitally signed .apk file's public certificate:

  1. Go to the Google Developers Console .

    Note: Create a single project for the Android, iOS and web versions of your app.

  2. Click Create Project:
    1. In the Project name field, type in a name for your project.
    2. In the Project ID field, optionally type in a project ID for your project or use the one that the console has created for you. This ID must be unique world-wide.
  3. Click the Create button and wait for the project to be created. Note: There may be short delay of up to 30 seconds before the project is created. Once the project is created, the name you gave it appears at the top of the left sidebar.
  4. In the left sidebar, select APIs & auth (the APIs sub-item is automatically selected).
    1. Find the Google+ API and set its status to ON—notice that this action moves Google+ API to the top of the list; you can scroll up to see it.
    2. Enable any other APIs that your app requires.
  5. In the sidebar under "APIs & auth", select Consent screen.
    1. Choose an Email Address and specify a Product Name.
  6. In the left sidebar under "APIs & auth", select Credentials.
    1. Click Create a new Client ID—the Create Client ID dialog box appears, as shown further below.
    2. Select Installed application for the application type.
    3. Select Android as the installed application type.
    4. Enter your Android app's package name . into the Package name field.
    5. In a terminal, run the the Keytool utility to get the SHA-1 fingerprint of the certificate. For the debug.keystore, the password is android.
      keytool -exportcert -alias androiddebugkey -keystore <path-to-debug-or-production-keystore> -list -v

      Keytool prints the fingerprint hash to the shell. For example:

      $ keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore -list -v
      Enter keystore password: Type "android" if using debug.keystore
      Alias name: androiddebugkey
      Creation date: Aug 27, 2012
      Entry type: PrivateKeyEntry
      Certificate chain length: 1
      Certificate[1]:
      Owner: CN=Android Debug, O=Android, C=US
      Issuer: CN=Android Debug, O=Android, C=US
      Serial number: 503bd581
      Valid from: Mon Aug 27 13:16:01 PDT 2012 until: Wed Aug 20 13:16:01 PDT 2042
      Certificate fingerprints:
         MD5:  1B:2B:2D:37:E1:CE:06:8B:A0:F0:73:05:3C:A3:63:DD
         SHA1: D8:AA:43:97:59:EE:C5:95:26:6A:07:EE:1C:37:8E:F4:F0:C8:05:C8
         SHA256: F3:6F:98:51:9A:DF:C3:15:4E:48:4B:0F:91:E3:3C:6A:A0:97:DC:0A:3F:B2:D2:E1:FE:23:57:F5:EB:AC:13:30
         Signature algorithm name: SHA1withRSA
         Version: 3
      

      Copy the SHA1 fingerprint hash from your terminal. The example above highlights where to find it.

    6. Paste the SHA-1 fingerprint hash into the Signing certificate fingerprint field shown below.
    7. To activate interactive posts, enable the Deep Linking option.

      Shows options in the Create Client ID dialog box.

    8. Click the Create client ID button.

You are done enabling the Google+ API for your app.

Step 2: Create an Eclipse project for PhotoHunt

To build and run the PhotoHunt client on your device:

  1. Clone or download the Android PhotoHunt client Git repository.

    git clone https://github.com/googleplus/gplus-photohunt-client-android.git
    
  2. Download ActionBarSherlock and unzip it.

  3. Launch Eclipse.
  4. Select File > Import > Android > Existing Android Code Into Workspace and click Next.
  5. Select Browse.... Enter the path to your PhotoHunt Android client directory.
    Displays the Eclipse import project dialog with the photohunt project selected.
  6. Import the ActionBarSherlock library project:
    1. Select File > Import > Android > Existing Android Code Into Workspace and click Next.
    2. Click Browse.... Enter <actionbarsherlock-folder>/actionbarsherlock/.
  7. Import the Google Play Services library project.
    1. Select File > Import > Android > Existing Android Code Into Workspace and click Next.
    2. Select Browse.... Enter <android-sdk-folder>/extras/google/google_play_services/.
    3. Select google-play-services_lib. Click Finish to import.
  8. Update the PhotoHunt project properties:
    1. Click Project > Properties. The project properties dialog displays.
    2. Select Java Build Path, and click the Libraries tab.
    3. Click Add External JARs, browse to <android-sdk-folder>/extras/android/support/v4/android-support-v4.jar. Click OK
    4. Select Android in the left navigation.
    5. Click Add... and select actionbarsherlock to add the ActionBarSherlock library project dependency.
    6. Click Add... and select google-play-service_lib to add the Google Play Services library project dependency.
      Displays the Eclipse library dependencies dialog with actionbarsherlock
 and Google Play Services selected.
    7. Click OK.
  9. Edit the src/config.properties file and modify the api_host property to be the hostname of a PhotoHunt server.
  10. Connect your Android device by USB to your system.
  11. Select Run > Run to test the PhotoHunt sample app. Choose Android application if prompted. The sample application will launch on your device.

Architecture of the application

The PhotoHunt client for Android that is driven primarily from a single view, which contains a stream of the photos in the currently selected hunt. The app runs on Android version 2.2 or later.

Architecture of the user interface

Graphic that shows the running Android PhotoHunt application.

  1. The main Activity for the PhotoHunt app is called ThemeViewActivity. The current theme title and buttons are displayed. The buttons change depending on whether the user is signed in or viewing the current theme. This Activity is the core point of interaction for the user.

  2. Each Activity subclasses BaseActivity, which handles the authenticated user state. The activity manages signing in and out of the application with both Google+ and the PhotoHunt web services. It delegates sign in with Google+ to PlusClientFragment and the Google+ SDK.

  3. The stream of photos is a ListView that is managed by the PhotoListAdapter object, which is the data source for the table. This object defines the View for each photo, including the vote and promote buttons, and displays the delete icon if the signed in user matches the photo uploader. The PhotoListAdapter object registers a View.onClickListener for those buttons that makes updates via the PhotoClient.

  4. Each individual cell of the stream ListView is created from the photo_list_item.xml layout.

  5. Menu items are added to the ActionBar by overriding onCreateOptionsMenu. The Google+ Sign-In button is added as a custom view.

  6. Tapping the About menu item transitions to the AboutActivity, which displays a simple static screen. The only dynamic element on the page is the version number of the application as defined in AndroidManifest.xml.

  7. Tapping the Your Activity menu item, which only appears for signed-in users, transitions to the ProfileActivity. The profile activity displays the signed in user's image and displays a ListView with the app activities that they have written.

Architecture of the data services

This app has several helper objects that manage the current state of the world for PhotoHunt.

  1. The ThemeViewActivity.ThemeListCallbacks, ThemeViewActivity.PhotoCallbacks and corresponding Loaders maintain the current theme state, and retrieve the data for the photo stream and the theme pickers.

  2. The PhotoHuntApp object contains an ImageLoader, which contains a cache of recently used images.

  3. PhotoClient is a wrapper that makes calls to the PhotoHunt back-end. The wrapper executes FetchJsonTask asynchronous task objects, and returns items or collections based on the response. The objects are all defined in the com.google.plus.samples.photohunt.model package, for example Photo, which represents a photo in PhotoHunt.

Setting up sign-in

To properly configure the PhotoHunt front-end clients and back-end web service, you must create all your client IDs in the same Developers Console project. Additionally, you will configure the sign-in buttons to request the same OAuth 2.0 scopes and request the same app activities types. This configuration allows the user to see exactly what the app is going to do on the authorization dialog.

In PhotoHunt for Android, the sign-in state of the user is determined by the PlusClient object. The scopes are requested when the PlusClient is created:

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // ...

    // Create the PlusClient.
    PlusClient.Builder plusClientBuilder =
            new PlusClient.Builder(getActivity().getApplicationContext(), this, this);
    String[] visibleActivities = getArguments().getStringArray(ARG_VISIBLE_ACTIVITIES);
    String[] scopes = getArguments().getStringArray(ARG_SCOPES);

    plusClientBuilder.setScopes(scopes);

    if (visibleActivities != null && visibleActivities.length > 0) {
        plusClientBuilder.setVisibleActivities(visibleActivities);
    }
    mPlusClient = plusClientBuilder.build();

    // ...

    // Attempt to connect.
    mPlusClient.connect();
    mIsConnecting = true;
}

Whenever PlusClient.connect is called, which is usually whenever an Activity is started, Google Play Services checks whether the user already picked an account to sign in with, and whether the PlusClient is able to get an OAuth 2.0 token for the selected account. If PlusClient is able to retrieve a token, or if it has previously cached a token that is still valid, the PlusClient will fire the onConnected event handler.

In PhotoHunt, the PlusClientFragment handles onConnected, and in this case fires onSignedIn:

public void onConnected() {
    // Successful connection!

// ...

    Activity activity = getActivity();
    if (activity instanceof OnSignInListener) {
        ((OnSignInListener) activity).onSignedIn(mPlusClient);
    }
}

There are several reasons why PlusClient might not have been able to fire the onConnected event:

  1. The user has not yet selected an account to sign in with.
  2. Google Play Services was unable to retrieve an OAuth token for some reason.
  3. The Google Play Services libraries may need updating.

In many cases, these issues can be resolved but in PhotoHunt the resolution is delayed until the user takes an action that requires sign in. In the onConnectionFailed method, the result of the connection attempt, and any potential resolution, is stored and resolution is only attempted if we are responding to a user action that requires sign in as indicated by mRequestCode != INVALID_REQUEST_CODE:

public void onConnectionFailed(ConnectionResult connectionResult) {
    mLastConnectionResult = connectionResult;
    mIsConnecting = false;

    // On a failed connection try again.
    if (isResumed() && mRequestCode != INVALID_REQUEST_CODE) {
        resolveLastResult();
    } else {
        Activity activity = getActivity();
        ((OnSignInListener) activity).onSignInFailed();
    }
}

BaseActivity responds to onSignedIn and onSignInFailed, and authenticates with the PhotoHunt web service by using the OAuth token provided by Google Play Services. Successful authentication against the PhotoHunt service is tracked in the mPhotoUser variable.

Displaying the sign-in button

For logged out users, PhotoHunt displays a stream of photos and displays a standard sign-in button at the top of the stream. The app displays this button in the action bar if the user is not currently authenticated and is not currently in the process of authenticating:

public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);

    getSupportActionBar().setDisplayShowCustomEnabled(false);

    if (isAuthenticated()) {
        // Show the 'sign out' menu item only if we have an authenticated
        // PhotoHunt profile.
        menu.add(0, R.id.menu_item_sign_out, 0,
            getString(R.string.sign_out_menu_title)).setShowAsAction(
                MenuItem.SHOW_AS_ACTION_NEVER);

        menu.add(0, R.id.menu_item_disconnect, 0,
            getString(R.string.disconnect_menu_title)).setShowAsAction(
                MenuItem.SHOW_AS_ACTION_NEVER);
    } else if (!isAuthenticated() && !isAuthenticating()) {
        ActionBar.LayoutParams params = new ActionBar.LayoutParams(
            ActionBar.LayoutParams.WRAP_CONTENT,
            ActionBar.LayoutParams.WRAP_CONTENT, Gravity.RIGHT);
        getSupportActionBar().setCustomView(mSignInButton, params);
        getSupportActionBar().setDisplayShowCustomEnabled(true);
    }
}

The button is defined in res/layout/sign_in_button.xml:

<com.google.android.gms.common.SignInButton
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:id="@+id/sign_in_button"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:layout_gravity="right" />

The button is created in BaseActivity.onCreate:

mSignInButton = (SignInButton) getLayoutInflater()
    .inflate(R.layout.sign_in_button, null);
mSignInButton.setOnClickListener(this);

When a user clicks the button, an authorization dialog displays and they are asked to approve the access that the app is requesting. As part of this dialog, the user can select which people from their circles that are shared with the application, and who can see the app activities PhotoHunt writes to Google.

Graphic that shows the consent dialogue on Android PhotoHunt.

Authenticating with PhotoHunt

The PhotoHunt Android client sends its OAuth 2.0 access token to the back-end server so that the server can perform various actions on the user's behalf such as retrieving the user's list of friends and writing app activities. To handle this process, the server must verify that the token is valid.

BaseActivity handles authenticating against the PhotoHunt service calling AuthUtil.authenticate when it receives an onSignedIn event from the PlusClientFragment:

public static User authenticate(Context ctx, String account) {
    // ...

    try {
        URL url = new URL(Endpoints.API_CONNECT);

        // Get an OAuth token from GoogleAuthUtil.  This will return
        // the currently cached token if it is still valid
        sAccessToken = GoogleAuthUtil.getToken(ctx, account, AuthUtil.SCOPE_STRING);

        // Format the token as a JSON structure
        byte[] postBody = String.format(ACCESS_TOKEN_JSON, sAccessToken).getBytes();

        // Send the token to the PhotoHunt server
        urlConnection = (HttpURLConnection) url.openConnection();

        // ...

        if (statusCode == 200) {

            // Read and store the session cookie returned by the PhotoHunt server

        } else {

            // Handle errors, for example, retry authentication if the server
            // responds with 401 'token expired'

        }
    } catch (...) {
        // ...
    }
}

GoogleAuthUtil will return the currently cached token even if it has expired. If the server responds with an expired token, we must invalidate the token and retry authentication.

At the end of the process, the BaseActivity stores the user's profile that is returned from the PhotoHunt web service, and triggers an update of the user interface:

public void onSignedIn(PlusClient plusClient) {
  if (plusClient.isConnected()) {
    mPlusPerson = plusClient.getCurrentPerson();

    // Retrieve the account name of the user which allows us to retrieve
    // the OAuth access token that we securely pass over to the PhotoHunt
    // service to identify and authenticate our user there.
    final String name = plusClient.getAccountName();

    // Asynchronously authenticate with the PhotoHunt service and
    // retrieve the associated PhotoHunt profile for the user.
    mAuthTask = new AsyncTask<Object, Void, User>() {
      @Override
      protected User doInBackground(Object... o) {
        return AuthUtil.authenticate(BaseActivity.this, name);
      }

      @Override
      protected void onPostExecute(User result) {
        if (result != null) {
          setAuthenticatedProfile(result);
          executePendingActions();
          update();
        } else {
          setAuthenticatedProfile(null);
          mPlus.signOut();
        }
      }
    };

    mAuthTask.execute();
  }
}

Signing out and disconnecting

After the user is signed in, they can tap Sign out in the menu to log out of the app. This triggers a call to the onOptionsItemSelected method in the BaseActivity:

case R.id.menu_item_sign_out:
  mPlus.signOut();
  // Invalidate the PhotoHunt session
  AuthUtil.invalidateSession();
  return true;

The invalidateSession method on the AuthUtil class drops the session cookie that is used to access the PhotoHunt web service. The application level sign out is handled by the signOut method on the PlusClientFragment:

public void signOut() {
  if (mPlusClient.isConnected()) {
    mPlusClient.clearDefaultAccount();
  }

  if (mIsConnecting || mPlusClient.isConnected()) {
    mPlusClient.disconnect();
    // Reconnect to get a new mPlusClient.
    mLastConnectionResult = null;
    // Cancel sign in.
    mRequestCode = INVALID_REQUEST_CODE;

    // Reconnect to fetch the sign-in (account chooser) intent from the plus client.
    mPlusClient.connect();
  }
}

This method clears the default account so that the user will need to choose which account to use the next time that they sign in. As this does not affect currently connected instances of PlusClient. the code next calls disconnect() and connect() on the PlusClient to reset the app to the inital state.

In some cases, users will want to fully disconnect the application. The Google+ developer policies requires that apps offer this disconnect functionality. Often apps will be able to use the built in disconnect method, PlusClient.revokeAccessAndDisconnect. Because PhotoHunt has a web service and stores user information on the back-end server, we must clear all the user's data from the server as well as from the app.

The disconnect option is offered in the menu and the click is handled in the BaseActivity.onOptionItemSelected method. When the PhotoClient.disconnectAccount method is called, the app sends a HTTP POST request to the PhotoHunt web service disconnect endpoint, and on sucess the users is signed out as above.

Querying the Google+ APIs

If the user clicks the profile icon in the menu, the app shows the ProfileActivity, which displays a list of their recent actions. These activities are retrieved from the app activities that have been written by PhotoHunt. All of the app activities are written server side.

The ProfileActivity.setAuthenticatedProfile method triggers retrieving the list of app activities by calling the PlusClients.loadMoments method. This method retrieves app activities written by PhotoHunt for the currently authenticated user.

The PlusClient.loadMoments method calls the onMomentsLoaded callback when it completes:

public void onMomentsLoaded(ConnectionResult status, MomentBuffer moments, String nextPageToken, String updated) {
  if (status.getErrorCode() == ConnectionResult.SUCCESS) {
    try {
      for (Moment moment : moments) {
        // Make the activities available to our adapter.
        // Each moment must be frozen in order to persist it outside of the
        // MomentBuffer.
        mListItems.add(moment.freeze());
      }
    } finally {
      moments.close();
    }

    mMomentListAdapter.notifyDataSetChanged();
  } else {
    Log.e(TAG, "Error when loading moments: " + status.getErrorCode());
  }
}

The mMomentListAdapter is an ArrayAdapter that extracts the name that is stored with each app activity for display. This name will be the title of the page that is associated with the photo for PhotoHunt app activities.

Moment moment = items.get(position);
if (moment != null) {
    TextView typeView = (TextView)v.findViewById(R.id.moment_type);
    TextView titleView = (TextView)v.findViewById(R.id.moment_title);
    typeView.setText("Voted");

    if (moment.getTarget() != null) {
        titleView.setText(moment.getTarget().getName());
    }
}

Creating interactive posts

Each photo that is displayed in the stream has a Promote button that allows users to share an interactive post on Google+ to encourage their friends to vote on the photo. The button is defined in the res/layout/photo_list_item.xml file. In the PhotoListAdapter.getView method, the onClickListener is set to trigger the enhanced share:

holder.promoteButton.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {
    if (!mBaseActivity.mPlus.isAuthenticated()) {
      mBaseActivity.requireSignIn();
      mBaseActivity.mPendingClick = this;
      mBaseActivity.mPendingView = view;
      return;
    }

    Intent interactivePostIntent = Intents.getInteractiveIntent(mBaseActivity,
                  metadata, mBaseActivity.mPlus.getClient(), mTheme, isActive);
    mBaseActivity.startActivityForResult(interactivePostIntent, 0);
  }
});

The onClick method first checks to see if the user is signed in. If not, the click event and the active photo view are stored, and the requireSignIn method is triggered on the BaseActivity to sign the user in. After the user is signed in, the BaseActivity.executePendingActions method is called and checks whether the mPendingClick member is null. If a click event was stored, the method calls the onClick method again.

If the user is signed in, then the interactive post intent is created. The intent is defined in the Intents.getInteractiveIntent method. This method uses the PlusShare.builder method to create the intent and configure it with the data that is supplied by the Photo object:

  Uri photoContentUri = Uri.parse(metadata.photoContentUrl);

  // Include the theme name in the share text.
  String shareText = activity.getString(R.string.vote_share_text);
  if (activeTheme != null) {
    shareText = String.format(activity.getString(R.string.vote_share_theme_text), generateThemeTag(activeTheme.displayName));
  }

  // Create an interactive post builder with the call to action metadata.
  PlusShare.Builder builder = new PlusShare.Builder(activity, plusClient);

To build an interactive share, the addCallToAction method is called with the post label of Vote.

  // Create the deep link URL
  String deepLink = "/?id=" + metadata.id;

  if (vote) {
    // Add the call-to-action metadata.
    builder.addCallToAction("VOTE", photoContentUri, metadata.voteCtaUrl);

    // Set the target deep-link ID (for mobile use).
    builder.setContentDeepLinkId(deepLink + "&action=VOTE", null, null, null);
  } else {
    // ...
  }

The URL for the 'title' link on the post can be different than the URL for the call-to-action button.

We return the constructed intent by using the builder.getIntent() method.

  // Set the target url (for desktop use).
  builder.setContentUrl(photoContentUri);

  // Set the pre-filled message.
  builder.setText(shareText);

  return builder.getIntent();

Additional resources

Send feedback about...

Google+ Platform