OAuth2

The purpose of this sample is to show how to use the OAuth 2.0 protocol for authentication into the ArcGIS platform via the sample. Once authenticated the app will return all the web maps associated with the account. You can then select a web map to open it up in a MapView.

Features

  • OAuthView
  • User Credentials
  • Web Map
  • clientID

Sample Requirements

In order to work with OAuth2 you need to edit the string resource file and assign a valid client id string to the client_id parameter. Follow these steps:

  • Browse to the ArcGIS developers site.
  • Sign in with your ArcGIS developer account.
  • Create an application. This will give you access to a client id string.
  • Initialize the client_id string resource, res/values/strings.xml, with the client id string and run the sample.

Sample Design

The sample consists of two FragmentActivity classes. The OAuth2Sample class checks if there are any previously saved UserCredentials. If credentials are not previously cached the app creates an instance of OAuthView and prompts the user to enter credentials. Once successfully authenticated the server returns the credentials in the onCallback method of the CallbackListener. The UserCredentials object is first encrypted by creating an instance of SealedObject and then serialized to the sdcard. The OAuth2Sample class then launches the UserContentActivity. If UserCredentials are present on the sdcard then OAuth2Sample class simply calls UserContentActivity class and bypasses all other steps.

The UserContentActivity class queries ArcGIS Portal for web maps with the user account and puts them in an ArrayList of UserWebmaps. Using the FragmentManager the app adds a new instance of UserContentFragment to the Activity. The UserContentFragment uses the userPortalDataList to populate an instance of UserContentArrayAdapter and display the thumbnail image, title, and description of each web map in the userPortalDataList. All the user interactions in the UserContentFragment are handled by the onFragmentInteraction method of OnFragmentInteractionListener interface which UserContentActivity implements. When the user taps on an item in the list, the onFragmentInteraction method is called, passing in the itemid of the item selected. This itemid is then put into a Bundle instance and passed on to the MapFragment. FragmentTransaction then replaces the UserContentFragment with the MapFragment.

The web map is constructed in the MapFragment onCreate method. The web map is created using the item chosen in the item list, when the callback returns successfully the web map is added to the MapView. The MapView instance is then added to the view of the the MapFragment.

Sample Code

/* Copyright 2014 ESRI
 *
 * All rights reserved under the copyright laws of the United States
 * and applicable international laws, treaties, and conventions.
 *
 * You may freely redistribute and use this sample code, with or
 * without modification, provided you include the original copyright
 * notice and use restrictions.
 *
 * See the Sample code usage restrictions document for further information.
 *
 */

package com.arcgis.android.samples.oauth2sample;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import javax.crypto.SecretKey;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.http.SslError;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.webkit.SslErrorHandler;

import com.esri.android.oauth.OAuthView;
import com.esri.android.oauth.OAuthView.OnSslErrorListener;
import com.esri.core.io.UserCredentials;
import com.esri.core.map.CallbackListener;

/**
 * THIS SAMPLE APP DOES NOT IMPLEMENT AN ENCRYPTION PATTERN. THE USER CREDENTIALS ARE NOT ENCRYPTED WHEN 
 * THEY ARE SAVED ON DISK.
 * IT IS THE RESPONSIBILITY OF THE APP DEVELOPER TO IMPLEMENT A SUITABLE ENCRYPTION PATTERN WHEN STORING USER
 * CREDENDTIALS ON DISK TO MAKE SURE THE APP IS SECURE.
 * READ MORE ABOUT ENCRYPTION PATTERNS HERE:
 * TODO : Paste blogpost link here
 */

/**
 * This sample shows how to use OAuth2 to authenticate against an ArcGIS Portal.
 * Follow these steps to get a client id:
 * <ol>
 * <li>Browse to https://developers.arcgis.com.</li>
 * <li>Sign in with your ArcGIS developer account.</li>
 * <li>Create an application. This will give you access to a client id string.</li>
 * <li>Replace CLIENT_ID constant in your string resource file with the client
 * id string and run the sample.</li>
 * </ol>
 */

public class OAuth2Sample extends FragmentActivity {

  protected static final String TAG = "OAuth2Sample";

  public static final String EXIT = "EXIT";

  SecretKey mSecretKey;

  // UI components
  private AlertDialog mAlertDialog;
  private AlertDialog mImplementEncryptionDialog;

  // File based componentes
  public static String mCredentialsFileName;

  // ArcGIS components

  private static UserCredentials mUserLoginCredentials;

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

    // Intent sent from UserContentActivity to Quit the Activity and app
    if (getIntent().getBooleanExtra(EXIT, false)) {
      finish();
      return;
    }

    // Get the credentials file name
    mCredentialsFileName = getResources().getString(R.string.credentials_filename);

    // Setup the alert dialog for determining new login or login with
    // credentials from internal storage
    setupLoginTypeAlertDialog();
    
    // Setup the alert dialog to inform users to implement 
    setupEncryptionPatternAlertDialog();


    // If the credentials file exists on internal storage, give user an option
    // to load those credentials
    if (fileExists(mCredentialsFileName)) {
      mAlertDialog.show();
    } else {
      // Prompt user to login
      showOAuth(OAuth2Sample.this);
    }
  }

  /**
   * display OAuthView in a popup and prompt user to login
   * 
   * @param context
   */
  void showOAuth(Context context) {

    // Create an instance of OAuthView
    // set client_id in string resource
    OAuthView oAuthView = new OAuthView(context, getResources().getString(R.string.portal_url), getResources()
        .getString(R.string.client_id), new CallbackListener<UserCredentials>() {

      @Override
      public void onError(Throwable e) {
        Log.e(TAG, "", e);
      }

      @Override
      public void onCallback(UserCredentials credentials) {

        // set UserCredentials
        setCredentials(credentials);
        
        try {
          // Save the credentials on the internal storage
          encryptAndSaveCredentials();
        } catch (Exception e) {
          Log.e(TAG, "Exception while saving User Credentials.", e);
        }

        runOnUiThread(new Runnable() {
          public void run() {
            mImplementEncryptionDialog.show();
          }
        });
        
      }
    });

    // handle SSL errors
    oAuthView.setOnSslErrorListener(new OnSslErrorListener() {
      @Override
      public void onReceivedSslError(OAuthView view, SslErrorHandler handler, SslError error) {
        Log.d(TAG, "" + error);
      }

    });

    // add OAuthview to the Activity
    ((ViewGroup) findViewById(R.id.main)).addView(oAuthView, new LayoutParams(LayoutParams.MATCH_PARENT,
        LayoutParams.MATCH_PARENT));

  }

  public boolean fileExists(String fname) {
    File file = getBaseContext().getFileStreamPath(fname);
    return file.exists();
  }

  /**
   * Encrypt and save user credentials on internal storage
   * 
   * THIS METHOD DOES NOT IMPLEMENT AN ENCRYPTION PATTERN. ITS THE RESPONSIBILITY OF THE 
   * DEVELOPER TO IMPLEMENT A SUITABLE ENCRYPTION PATTERN TO MAKE SURE THE APP IS SECURE.
   * READ ABOUT ENCRYPTION PATTERNS HERE:
   * TODO : Paste blogpost link here
   * 
   */
  void encryptAndSaveCredentials() throws Exception {

    // TODO : Implement encrypting the credentials before saving them to disk
    
    // write the encrypted user credentials to internal storage
    writeToFile(mCredentialsFileName, getCredentials());
  }

  /**
   * Method for writing object to a file
   * 
   * @param filename
   * @param object
   * @throws Exception
   */
  private void writeToFile(String filename, Object object) throws Exception {
    FileOutputStream fos = null;
    ObjectOutputStream os = null;

    try {
      fos = openFileOutput(filename, Context.MODE_PRIVATE);
      os = new ObjectOutputStream(fos);
      os.writeObject(object);
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      if (os != null) {
        try {
          os.close();
          fos.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  }

  /**
   * Setter for UserCredentials
   * 
   * @param credentials
   */
  public void setCredentials(UserCredentials credentials) {
    OAuth2Sample.mUserLoginCredentials = credentials;
  }

  /**
   * returns user credentials saved on internal storage
   * 
   * @return UserCredentials
   */
  public static UserCredentials getCredentials() {
    return mUserLoginCredentials;
  }

  /**
   * Loads the saved credentials from internal storage for re-login
   * 
   * THIS METHOD DOES NOT IMPLEMENT DECRYPTION PATTERN. ITS THE RESPONSIBILITY OF THE 
   * DEVELOPER TO IMPLEMENT A SUITABLE DECRYPTION PATTERN TO MAKE SURE THE APP IS SECURE.
   * READ ABOUT ENCRYPTION PATTERNS HERE:
   * TODO : Paste blogpost link here
   * 
   */
  void decryptAndloadCredentials() throws Exception {

    // TODO Decrypt the user credentials stored earlier on disk

    // set the credentials read from the file on interal storage
    setCredentials((UserCredentials) readFromFile(mCredentialsFileName));

  }

  /**
   * Method for reading object from a file
   * 
   * @param filename
   * @return
   * @throws Exception
   */
  private Object readFromFile(String filename) throws Exception {

    Object object = null;
    FileInputStream fis = null;
    ObjectInputStream is = null;

    try {
      fis = openFileInput(filename);
      is = new ObjectInputStream(fis);
      object = is.readObject();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      if (is != null) {
        try {
          is.close();
          fis.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
    // }

    return object;
  }

  public void setupLoginTypeAlertDialog() {

    AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(OAuth2Sample.this);

    // set title
    alertDialogBuilder.setTitle(getResources().getString(R.string.OAuth2_Sample));

    // set dialog message
    alertDialogBuilder.setMessage(getResources().getString(R.string.continue_with_saved_credentials))
        .setCancelable(false)
        .setPositiveButton(getResources().getString(R.string.yes), new DialogInterface.OnClickListener() {
          @Override
          public void onClick(DialogInterface dialog, int id) {
            // if this button is clicked, load credentials
            // from internal storage
            try {
              decryptAndloadCredentials();
            } catch (Exception e) {
              e.printStackTrace();
            }
            // Start UserContentActivity
            Intent i = new Intent(getApplicationContext(), UserContentActivity.class);
            startActivity(i);

          }
        }).setNegativeButton(getResources().getString(R.string.no), new DialogInterface.OnClickListener() {
          @Override
          public void onClick(DialogInterface dialog, int id) {
            // close the dialog box and showOAuthview again
            dialog.cancel();
            // Start OAuthView again
            showOAuth(OAuth2Sample.this);
          }
        });

    // create alert dialog
    mAlertDialog = alertDialogBuilder.create();

  }
  
  public void setupEncryptionPatternAlertDialog() {

    AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(OAuth2Sample.this);

    // set title
    alertDialogBuilder.setTitle(getResources().getString(R.string.OAuth2_Sample));

    // set dialog message
    alertDialogBuilder.setMessage(getResources().getString(R.string.implement_an_encryption_pattern))
        .setCancelable(false)
        .setPositiveButton(getResources().getString(R.string.ok), new DialogInterface.OnClickListener() {
          @Override
          public void onClick(DialogInterface dialog, int id) {
            
            // Start UserContentActivity
            Intent i = new Intent(getApplicationContext(), UserContentActivity.class);
            startActivity(i);

          }
        });

    // create alert dialog
    mImplementEncryptionDialog = alertDialogBuilder.create();

  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
  }

  @Override
  protected void onPause() {
    super.onPause();
    mAlertDialog.dismiss();
  }

  @Override
  protected void onResume() {
    super.onResume();
  }

}
/* Copyright 2014 ESRI
 *
 * All rights reserved under the copyright laws of the United States
 * and applicable international laws, treaties, and conventions.
 *
 * You may freely redistribute and use this sample code, with or
 * without modification, provided you include the original copyright
 * notice and use restrictions.
 *
 * See the Sample code usage restrictions document for further information.
 *
 */

package com.arcgis.android.samples.oauth2sample;

import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.FrameLayout;
import android.widget.ProgressBar;
import android.widget.Toast;

import com.esri.android.map.MapView;
import com.esri.android.map.event.OnStatusChangedListener;
import com.esri.core.map.CallbackListener;
import com.esri.core.portal.WebMap;

public class MapFragment extends Fragment {

  protected static final String TAG = "MapFragment";

  private String mItemId;

  @SuppressWarnings("unused")
  private OnFragmentInteractionListener_MapFragment mListener;

  View mUserMapView;

  MapView mMap;

  ProgressBar mProgressBar;

  protected static final int CLOSE_LOADING_WINDOW = 0;

  /**
   * factory method to create a new instance of this fragment using the provided
   * parameters.
   * 
   * @param itemId
   * @return A new instance of fragment MapFragment.
   */
  public static MapFragment newInstance(String itemId) {
    MapFragment fragment = new MapFragment();
    Bundle args = new Bundle();
    args.putString("ItemId", itemId);
    fragment.setArguments(args);
    return fragment;
  }

  public MapFragment() {
    // Required empty public constructor
  }

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (getArguments() != null) {
      mItemId = getArguments().getString("ItemId");
    }

    // create a new instance of the webmap from item
    // the webmap will be created in the callback
    WebMap.newInstance(mItemId, UserContentActivity.mMyPortal, new CallbackListener<WebMap>() {

      @Override
      public void onError(Throwable e) {

        Log.e(TAG, "Error instantiating WebMap", e);
      }

      @Override
      public void onCallback(final WebMap webmap) {

        // Add the mapview in the ui thread.
        getActivity().runOnUiThread(new Runnable() {

          @Override
          public void run() {

            if (webmap != null) {
              mMap = new MapView(getActivity(), webmap, null, null);

              mMap.setOnStatusChangedListener(new OnStatusChangedListener() {

                private static final long serialVersionUID = 1L;

                @Override
                public void onStatusChanged(Object source, STATUS status) {
                  if (status.getValue() == EsriStatusException.INIT_FAILED_WEBMAP_UNSUPPORTED_LAYER) {

                    Toast.makeText(getActivity(), "Webmap failed to load", Toast.LENGTH_SHORT).show();
                  }

                }
              });
              // set the visibility of progress bar to
              // invisible
              mProgressBar.setVisibility(View.INVISIBLE);
              // add the mapview to the fragment
              ((ViewGroup) getView()).addView(mMap, new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
                  LayoutParams.MATCH_PARENT));
            }

          }
        });

      }
    });

  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    mUserMapView = inflater.inflate(R.layout.fragment_map, container, false);
    mProgressBar = (ProgressBar) mUserMapView.findViewById(R.id.progress);
    mProgressBar.setVisibility(View.VISIBLE);
    return mUserMapView;
  }

  @Override
  public void onAttach(Activity activity) {
    super.onAttach(activity);
    try {
      mListener = (OnFragmentInteractionListener_MapFragment) activity;
    } catch (ClassCastException e) {
      throw new ClassCastException(activity.toString() + " must implement OnFragmentInteractionListener");
    }
  }

  @Override
  public void onDetach() {
    super.onDetach();
    mListener = null;
  }

  @Override
  public void onDestroyView() {
    super.onDestroyView();

  }

  public interface OnFragmentInteractionListener_MapFragment {
    public void onFragmentInteraction(Uri uri);
  }

}
/* Copyright 2014 ESRI
 *
 * All rights reserved under the copyright laws of the United States
 * and applicable international laws, treaties, and conventions.
 *
 * You may freely redistribute and use this sample code, with or
 * without modification, provided you include the original copyright
 * notice and use restrictions.
 *
 * See the Sample code usage restrictions document for further information.
 *
 */

package com.arcgis.android.samples.oauth2sample;

import android.graphics.Bitmap;

import com.esri.core.portal.PortalItem;

public class UserWebmaps {
  public PortalItem item;
  public Bitmap itemThumbnail;

  public UserWebmaps(PortalItem webmap, Bitmap bt) {
    this.item = webmap;
    this.itemThumbnail = bt;
  }
}
/* Copyright 2014 ESRI
 *
 * All rights reserved under the copyright laws of the United States
 * and applicable international laws, treaties, and conventions.
 *
 * You may freely redistribute and use this sample code, with or
 * without modification, provided you include the original copyright
 * notice and use restrictions.
 *
 * See the Sample code usage restrictions document for further information.
 *
 */

package com.arcgis.android.samples.oauth2sample;

import java.util.ArrayList;
import java.util.List;

import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;

import com.arcgis.android.samples.oauth2sample.MapFragment.OnFragmentInteractionListener_MapFragment;
import com.arcgis.android.samples.oauth2sample.UserContentFragment.OnFragmentInteractionListener;
import com.esri.core.io.UserCredentials;
import com.esri.core.portal.Portal;
import com.esri.core.portal.PortalItem;
import com.esri.core.portal.PortalItemType;
import com.esri.core.portal.PortalUser;
import com.esri.core.portal.PortalUserContent;

public class UserContentActivity extends FragmentActivity implements OnFragmentInteractionListener,
    OnFragmentInteractionListener_MapFragment {

  protected static final String TAG = "UserContentActivity";

  // ArcGIS components
  public static ArrayList<UserWebmaps> mUserPortalDataList;

  UserCredentials mValidLoginCredentials;

  public static Portal mMyPortal;

  // UI components
  AlertDialog mAlertDialog;

  static ProgressDialog mProgressDialog;

  protected static final int CLOSE_LOADING_WINDOW = 0;

  // Handler to close loading window
  final Handler uihandler = new HandlerExtension();

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    Log.d("UserContentActivity", "inside UserContentActivity");

    mValidLoginCredentials = OAuth2Sample.getCredentials();

    setupQuitAppAlertDialog();

    mProgressDialog = ProgressDialog
        .show(this, "WebMaps from arcgis.com ", "Getting User WebMaps from portal ........");

    new GroupTask().execute();

  }

  private final static class HandlerExtension extends Handler {

    // default constructor
    public HandlerExtension() {
    }

    @Override
    public void handleMessage(Message msg) {
      switch ((msg.what)) {
      case CLOSE_LOADING_WINDOW:

        if (mProgressDialog != null)
          mProgressDialog.dismiss();
        break;

      default:
        break;
      }

    }
  }

  public class GroupTask extends AsyncTask<Void, Void, Void> {

    @Override
    protected Void doInBackground(Void... params) {
      try {
        getUserContentFromPortal();
      } catch (Exception e) {
        Log.e(TAG, "Exception while getting content from portal", e);
      }

      return null;
    }

  }

  public void getUserContentFromPortal() throws Exception {

    UserWebmaps userWebMaps;
    mMyPortal = new Portal(getResources().getString(R.string.portal_url), mValidLoginCredentials);
    mUserPortalDataList = new ArrayList<UserWebmaps>();

    // Fetch user from the portal and get user contents

    PortalUser user = mMyPortal.fetchUser();
    PortalUserContent puc = user.fetchContent();
    List<PortalItem> items = puc.getItems();

    if (items == null) {
      Log.e(TAG, "No items returned by Portal for the user");
      return;
    }

    // Get only the webmaps in the user account and add them in the ArrayList
    for (PortalItem item : items) {
      Log.i(TAG, "Item id = " + item.getTitle());
      if (item.getType() == PortalItemType.WEBMAP) {
        byte[] data = item.fetchThumbnail();
        Bitmap bitmap;
        if (data != null) {
          bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
        } else {
          bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.map);
        }
        userWebMaps = new UserWebmaps(item, bitmap);
        Log.i(TAG, "Item id = " + item.getTitle());
        mUserPortalDataList.add(userWebMaps);
      }

    }

    Log.d(TAG, "userPortalDataList" + mUserPortalDataList);
    uihandler.sendEmptyMessage(CLOSE_LOADING_WINDOW);
    UserContentActivity.this.runOnUiThread(new Runnable() {

      @Override
      public void run() {

        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();

        UserContentFragment ucf = UserContentFragment.newInstance();
        ft.add(android.R.id.content, ucf, "user_content_fragment");
        ft.addToBackStack("user_content_fragment");
        ft.commit();
      }
    });

  }

  public void setupQuitAppAlertDialog() {

    AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(UserContentActivity.this);

    // set title
    alertDialogBuilder.setTitle(getResources().getString(R.string.OAuth2_Sample));

    // set dialog message
    alertDialogBuilder.setMessage(getResources().getString(R.string.quit_app)).setCancelable(false)
        .setPositiveButton(getResources().getString(R.string.yes), new DialogInterface.OnClickListener() {
          @Override
          public void onClick(DialogInterface dialog, int id) {
            // if this button is clicked, close
            // current activity
            Intent intent = new Intent(getApplicationContext(), OAuth2Sample.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            intent.putExtra(OAuth2Sample.EXIT, true);
            startActivity(intent);
          }
        }).setNegativeButton(getResources().getString(R.string.no), new DialogInterface.OnClickListener() {
          @Override
          public void onClick(DialogInterface dialog, int id) {
            // if this button is clicked, just close
            // the dialog box and do nothing
            dialog.cancel();
          }
        });

    // create alert dialog
    mAlertDialog = alertDialogBuilder.create();

    // show it

  }

  @Override
  public void onFragmentInteraction(String id) {

    Log.d(TAG, "on Fragment Interaction");

    MapFragment mapFragment = MapFragment.newInstance(id);

    FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
    transaction.replace(android.R.id.content, mapFragment, "mapfragment");
    transaction.addToBackStack("mapfragment");

    // Commit the transaction
    transaction.commit();

  }

  // Fragment interaction listener for MapFragment
  // we don't do anything in this method as we don't want to perform any
  // action on the map
  @Override
  public void onFragmentInteraction(Uri uri) {

  }

  @Override
  public void onBackPressed() {

    Fragment f = getSupportFragmentManager().findFragmentByTag("user_content_fragment");
    if (f.isVisible()) {
      mAlertDialog.show();
      return;
    }
    super.onBackPressed();

  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
  }

  @Override
  protected void onPause() {
    super.onPause();
    mAlertDialog.dismiss();
  }

  @Override
  protected void onResume() {
    super.onResume();
  }

}
Feedback on this topic?