Portal Featured User Groups

The primary purpose of this sample is to show how to log in to a portal, fetch info about the featured groups, fetch info about webmap items in a particular group, then fetch and display a particular webmap.

A secondary purpose is to demonstrate two different techniques for handling device configuration changes.

Features

  • Portal API
  • User Credentials
  • Simple Async Task
  • Fetch Groups
  • Fetch Group Thumbnail
  • Fetch Portal Items
  • Fetch Portal Item Thumbnail
  • Fetch WebMap
  • Create and Display MapView
  • Device Configuration Changes

How to use the Sample

The sample starts by logging in to the portal and displaying a list of featured groups. When you select a group it fetches and displays a list of web map items in the selected group. When you select an item it fetches and displays the web map. Press the Back key repeatedly to exit the app.

Sample Design

This sample contains two activities, FeaturedGroupsActivity and MapActivity. FeaturedGroupsActivity hosts two fragments, GroupsFragment and ItemsFragment.

FeaturedGroupsActivity

FeaturedGroupsActivity is the main activity of this sample. It simply hosts the GroupsFragment and the ItemsFragment. On startup it launches a GroupsFragment.

GroupsFragment

GroupsFragment is responsible for logging in to the portal and displaying a list or grid of featured groups.

It handles device configuration changes, for example changing between portrait and landscape orientation, by calling setRetainInstance(true) in its onCreate() method. This stops the fragment from being destroyed when its activity is destroyed and recreated, so its Portal and PortalGroup objects are retained.

The onCreateView() method creates the view from the list_layout layout resource. The layout/list_layout.xml file contains a ListView for use in portrait orientation, but layout-land/list_layout.xml contains a GridView for use in landscape orientation. An instance of private class FeaturedGroupListAdapter is set as the list adapter.

The onViewCreated() method executes an AsyncTask to do most of the work. A private class FeaturedGroupsAsyncTask extends AsyncTask and overrides the following methods:

  • onPreExecute() displays a progress dialog on the UI thread.
  • doInBackground(), which runs on a background thread, logs in to the server and fetches information about the featured groups. It then fetches the thumbnail for each group. The group title and thumbnail are saved in mFeaturedGroups, the array list which backs the list adapter.
  • onPostExecute() then displays the information on the UI thread by simply calling notifyDataSetChanged() on the list adapter.

A Portal object is created using the constructor which takes portal URL and credentials. The fetchPortalInfo() method is used to log in to the portal. This returns a PortalInfo object and its getFeaturedGroupsQueries() method provides a list of queries we can use to find the featured groups. Each query in turn is submitted using the Portal.findGroups() method. For each group, its thumbnail (if any) is fetched by calling fetchThumbnail().

When the user chooses a group, the onItemClick() method of the OnItemClickListener setup in onCreateView() creates and launches an ItemsFragment, passing it the Portal object and the PortalGroup of the chosen group.

ItemsFragment

ItemsFragment has a very similar structure to GroupsFragment. It calls setRetainInstance(true) to retain the Portal, PortalGroup and PortalItem objects it uses. It creates its view from the list_layout layout resource, so this is a ListView or GridView dependant on orientation. It uses private classes to provide a list adapter and an AsyncTask to do the work.

The work done on the background thread consists of creating a query to find all web maps in the chosen group and submitting this using the Portal.findItems() method. For each item found, its thumbnail (if any) is fetched by calling fetchThumbnail(). The item title and thumbnail are saved in mItems, the array list which backs the list adapter.

When the user chooses a web map, the onItemClick() method of the OnItemClickListener setup in onCreateView() launches a new activity, MapActivity, and passes it the item ID of the chosen map.

MapActivity

A separate activity, MapActivity, is used to display the map (see below for why). It displays a progress dialog and creates a WebMap by calling the newInstance() method that takes an itemId, Portal and CallbackListener. The CallbackListener’s onCallback() method is called when the WebMap has been created. It runs a Runnable on the UI thread that creates a MapView and uses it to display the WebMap. It sets an OnStatusChangedListener on the MapView so that it can dismiss the progress dialog when initialisation of the MapView is complete.

Device Configuration Changes

The reason for using a separate activity to display the map is to optimise behaviour on device configuration changes. GroupsFragment and ItemsFragment use Fragment.setRetainInstance() to retain Portal, PortalGroup and PortalItem objects when their host activity is destroyed on configuration changes. However this technique would not work well for the WebMap object used by MapActivity because it becomes tied to the MapView used to display it and a new MapView must be created if the host activity is destroyed and recreated.

We solve this problem by isolating use of our WebMap in a separate activity, MapActivity, and specifying the android:configChanges attribute for MapActivity as follows in the app’s manifest file:

android:configChanges="orientation|screenSize|keyboard|keyboardHidden

This stops MapActivity from being destroyed and restarted when orientation and keyboard configuration changes occur.

So why don't we use this technique in GroupsFragment and ItemsFragment? These fragments make use of config-dependent resources, in particular using different layout files for different device orientations. If we specified the android:configChanges attribute as above for the activity hosting these fragments, we would need to write our own code to switch the config-dependent resources on each configuration change, rather than letting the Android system handle that for us. It would not be much work to do the switch of our one layout file ourselves, but some apps may make much more extensive use of config-dependent layouts, strings, drawables, dimensions, etc. This app shows how such apps can avoid having to handle the config changes themselves.

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.esri.arcgis.android.samples.featuredusergroup;

import android.app.Activity;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.util.Log;
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.Portal;
import com.esri.core.portal.WebMap;

/**
 * This activity is launched when the user chooses an item from the list of webmaps in a group. The
 * portal item ID is passed in via the Intent that starts this activity.
 * <p>
 * It fetches the webmap and displays it in a MapView.
 * <p>
 * NOTE: The reason for using a separate activity to display the map is to optimize behavior on
 * device configuration changes. GroupsFragment and ItemsFragment use Fragment.setRetainInstance()
 * to retain Portal, PortalGroup and PortalItem objects when their host activity is destroyed on
 * configuration changes. However this technique would not work well for the WebMap object used by
 * MapActivity because it becomes tied to the MapView used to display it and a new MapView must be
 * created if the host activity is destroyed and recreated.
 * <p>
 * We solve this problem by isolating use of our WebMap in a separate activity, MapActivity, and
 * specifying the android:configChanges attribute for MapActivity as follows in the app�s manifest
 * file:
 * <p>
 * android:configChanges="orientation|screenSize|keyboard|keyboardHidden�
 * <p>
 * This stops MapActivity from being destroyed and restarted when orientation and keyboard
 * configuration changes occur.
 */
public class MapActivity extends Activity {
  public static final String KEY_PORTAL_ITEM_ID = "com.esri.arcgis.android.samples.ItemId";

  private static final String TAG = "MapActivity";

  ProgressDialog mProgressDialog = null;

  
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    // Get the item ID from the Intent that started this activity
    String itemId = getIntent().getExtras().getString(KEY_PORTAL_ITEM_ID);

    // Setup and show progress dialog
    mProgressDialog = new ProgressDialog(this) {
      @Override
      public void onBackPressed() {
        // Back key pressed - dismiss the dialog and finish the activity
        mProgressDialog.dismiss();
        finish();
      }
    };
    mProgressDialog.setMessage(getString(R.string.fetchingMap));
    mProgressDialog.show();

    // Get Portal object from ItemsFragment, but beware it could be null if the system has forced
    // ItemsFragment to be destroyed and recreated
    Portal portal = ItemsFragment.getPortal();
    if (portal == null) {
      // Just finish this activity if no Portal object available
      finish();
      return;
    }

    // Create a new instance of WebMap
    WebMap.newInstance(itemId, portal, new CallbackListener<WebMap>() {

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

      @Override
      public void onCallback(final WebMap webmap) {

        // The WebMap has been created - switch to UI thread to create MapView
        runOnUiThread(new Runnable() {

          @Override
          public void run() {

            // Create a MapView from the WebMap
            if (webmap != null) {
              MapView map = new MapView(MapActivity.this, webmap, null, null);

              map.setOnStatusChangedListener(new OnStatusChangedListener() {

                private static final long serialVersionUID = 1L;

                @Override
                public void onStatusChanged(Object source, STATUS status) {
                  switch (status) {
                    case INITIALIZED:
                      // MapView initialization complete so dismiss the progress dialog
                      mProgressDialog.dismiss();
                      break;
                    case INITIALIZATION_FAILED:
                      Toast.makeText(MapActivity.this, getString(R.string.webmapLoadFailed),
                          Toast.LENGTH_LONG).show();
                      break;
                    case LAYER_LOADED:
                    case LAYER_LOADING_FAILED:
                      break;
                  }
                }
              });

              // Display the MapView
              setContentView(map);
            }

          }
        });

      }
    });

  }
}
/* 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.esri.arcgis.android.samples.featuredusergroup;

import android.app.Activity;
import android.app.FragmentManager;
import android.os.Bundle;

/**
 * The primary purpose of this sample is to show how to log in to a portal, fetch info about the
 * featured groups, fetch info about webmap items in a particular group, then fetch and display a
 * particular webmap.
 * <p>
 * A secondary purpose is to demonstrate two different techniques for handling device configuration
 * changes.
 * <p>
 * This is the main activity of this sample. It simply hosts the GroupsFragment and the
 * ItemsFragment. On startup it launches a GroupsFragment.
 */
public class FeaturedGroupsActivity extends Activity {

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

    // Set content view and setup action bar
    setContentView(R.layout.featured_groups_activity);
    getActionBar().setDisplayHomeAsUpEnabled(false);
    getActionBar().setHomeButtonEnabled(false);

    // Only if this is a fresh start, kick off the GroupsFragment
    if (savedInstanceState == null) {
      FragmentManager fragMgr = getFragmentManager();
      fragMgr.beginTransaction().add(R.id.main_fragment_container, new GroupsFragment(), null)
          .commit();
    }
  }

}
/* 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.esri.arcgis.android.samples.featuredusergroup;

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

import android.app.Fragment;
import android.app.FragmentManager;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import com.esri.core.io.UserCredentials;
import com.esri.core.portal.Portal;
import com.esri.core.portal.PortalGroup;
import com.esri.core.portal.PortalInfo;
import com.esri.core.portal.PortalQueryParams;
import com.esri.core.portal.PortalQueryResultSet;

/**
 * This fragment is launched when FeaturedGroupsActivity starts. It logs in to the portal and
 * displays a list of the featured groups. It launches ItemsFragment when the user picks a group.
 */
public class GroupsFragment extends Fragment {

  private static final String TAG = "GroupsFragment";

  private static final String URL = "http://arcgis.com";

  private static final String USER_NAME = "democsf";

  private static final String PASSWORD = "devdemo";

  static Portal mPortal;

  FeaturedGroupListAdapter mAdapter;

  ArrayList<FeaturedGroup> mFeaturedGroups;

  ProgressDialog mProgressDialog;

  /**
   * Mandatory empty constructor for the fragment manager to instantiate the fragment (e.g. upon
   * device configuration changes).
   */
  public GroupsFragment() {
  }

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

    // Calling setRetainInstance() causes the Fragment instance to be retained when its Activity is
    // destroyed and recreated. This allows some ArcGIS objects (Portal and PortalGroup) to be
    // retained so data will not need to be fetched from the network again.
    setRetainInstance(true);

    // Setup list view and list adapter
    if (mFeaturedGroups == null || mAdapter == null) {
      mFeaturedGroups = new ArrayList<FeaturedGroup>();
      mAdapter = new FeaturedGroupListAdapter(getActivity(), mFeaturedGroups);
    }
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // Creates the view from the list_layout layout resource. The layout/list_layout.xml file
    // contains a ListView for use in portrait orientation, but layout-land/list_layout.xml contains
    // a GridView for use in landscape orientation
    View view = inflater.inflate(R.layout.list_layout, container, false);

    // Setup title
    TextView textView = (TextView) view.findViewById(R.id.listTitleTextView);
    textView.setText(R.string.featuredGroupsTitle);

    // Setup list view - maybe a ListView or a GridView
    AbsListView list = (AbsListView) view.findViewById(android.R.id.list);
    list.setAdapter(mAdapter);
    list.setOnItemClickListener(new OnItemClickListener() {

      @Override
      public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
        // Launch an ItemsFragment to handle the group the user has selected
        FragmentManager fragMgr = getFragmentManager();
        ItemsFragment fragment = new ItemsFragment();
        // Pass Portal and PortalGroup objects to the new fragment
        fragment.setParams(mPortal, mFeaturedGroups.get(position).group);
        fragMgr.beginTransaction().replace(R.id.main_fragment_container, fragment, null)
            .addToBackStack(null).commit();
      }

    });

    // Setup progress dialog
    mProgressDialog = new ProgressDialog(getActivity()) {
      @Override
      public void onBackPressed() {
        // Back key pressed - dismiss the dialog and finish the activity
        dismiss();
        getActivity().finish();
      }
    };
    return view;
  }

  @Override
  public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    if (mFeaturedGroups == null || mFeaturedGroups.size() == 0) {
      // Execute an async task to fetch and display the featured groups
      new FeaturedGroupsAsyncTask().execute();
    }
  }

  /**
   * This class provides an AsyncTask that fetches info about featured groups from the server on a
   * background thread and displays it in a list on the UI thread.
   */
  private class FeaturedGroupsAsyncTask extends AsyncTask<Void, Void, Void> {
    private Exception mException;

    public FeaturedGroupsAsyncTask() {
    }

    @Override
    protected void onPreExecute() {
      // Display progress dialog on UI thread
      mProgressDialog.setOnDismissListener(new OnDismissListener() {
        @Override
        public void onDismiss(DialogInterface arg0) {
          // Cancel the task if it's not finished yet
          FeaturedGroupsAsyncTask.this.cancel(true);
        }
      });
      mProgressDialog.setMessage(getString(R.string.fetchingGroups));
      mProgressDialog.show();
    }

    @Override
    protected Void doInBackground(Void... params) {
      mException = null;

      // Create UserCredentials with USER_NAME and PASSWORD
      UserCredentials credentials = new UserCredentials();
      credentials.setUserAccount(USER_NAME, PASSWORD);

      // Create a Portal object
      mPortal = new Portal(URL, credentials);

      try {
        // Fetch portal info from server. This logs in with the credentials set above
        PortalInfo portalInfo = mPortal.fetchPortalInfo();
        if (isCancelled()) {
          return null;
        }

        // Get list of queries to use to fetch the featured groups
        List<String> querys = portalInfo.getFeaturedGroupsQueries();

        // Loop through query list to find each featured group
        for (String query : querys) {
          Log.d(TAG, "[query] " + query);
          PortalQueryResultSet<PortalGroup> result = mPortal
              .findGroups(new PortalQueryParams(query));
          if (isCancelled()) {
            return null;
          }

          // Loop through query results
          for (PortalGroup group : result.getResults()) {
            Log.d(TAG, "[group title] " + group.getTitle());

            // Fetch group thumbnail (if any) from server
            byte[] data = group.fetchThumbnail();
            if (isCancelled()) {
              return null;
            }
            if (data != null) {
              // Add group to list only if we have a thumbnail
              Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
              mFeaturedGroups.add(new FeaturedGroup(group, bitmap));
            }
          }
        }
      } catch (Exception e) {
        mException = e;
      }
      return null;
    }

    @Override
    protected void onPostExecute(Void result) {
      // Display results on UI thread
      mProgressDialog.dismiss();
      if (mException != null) {
        Log.w(TAG, mException.toString());
        Toast.makeText(getActivity(), getString(R.string.fetchDataFailed), Toast.LENGTH_LONG)
            .show();
        getActivity().finish();
        return;
      }
      mAdapter.notifyDataSetChanged();
    }

  }

  /**
   * This class provides the adapter for the list of featured groups.
   */
  private class FeaturedGroupListAdapter extends ArrayAdapter<FeaturedGroup> {

    public FeaturedGroupListAdapter(Context context, ArrayList<FeaturedGroup> groupinfo) {
      super(context, 0, groupinfo);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
      View view = convertView;

      // Inflate view unless we've been given an existing view to reuse
      if (convertView == null) {
        LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(
            Context.LAYOUT_INFLATER_SERVICE);
        view = inflater.inflate(R.layout.list_item, null);
      }

      // Setup group thumbnail
      FeaturedGroup item = getItem(position);
      ImageView image = (ImageView) view.findViewById(R.id.itemThumbnailImageView);
      image.setImageBitmap(item.groupThumbnail);

      // Setup group title
      TextView text = (TextView) view.findViewById(R.id.itemTitleTextView);
      text.setText(item.group.getTitle());
      return view;
    }

  }

  /**
   * This class holds data for a featured group.
   */
  private class FeaturedGroup {
    PortalGroup group;

    Bitmap groupThumbnail;

    public FeaturedGroup(PortalGroup pg, Bitmap bt) {
      this.group = pg;
      this.groupThumbnail = 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.esri.arcgis.android.samples.featuredusergroup;

import java.util.ArrayList;

import android.app.Fragment;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import com.esri.core.portal.Portal;
import com.esri.core.portal.PortalGroup;
import com.esri.core.portal.PortalItem;
import com.esri.core.portal.PortalItemType;
import com.esri.core.portal.PortalQueryParams;
import com.esri.core.portal.PortalQueryResultSet;

/**
 * This fragment is launched when the user chooses a group from the list of featured groups. The
 * Portal and the chosen PortalGroup are passed in using the {@link #setParams(Portal, PortalGroup)}
 * method.
 * <p>
 * It displays a list of the webmap items in the chosen group and launches MapActivity when the user
 * picks an item.
 */
public class ItemsFragment extends Fragment {
  private static final String TAG = "ItemsFragment";

  PortalItemListAdapter mAdapter;

  ArrayList<PortalItemData> mItems;

  static Portal mPortal;

  PortalGroup mPortalGroup;

  ProgressDialog mProgressDialog;

  /**
   * Mandatory empty constructor for the fragment manager to instantiate the fragment (e.g. upon
   * device configuration changes).
   */
  public ItemsFragment() {
  }

  /**
   * Passes the Portal and the chosen PortalGroup into this fragment.
   * 
   * @param portal
   * @param portalGroup
   */
  public void setParams(Portal portal, PortalGroup portalGroup) {
    mPortal = portal;
    mPortalGroup = portalGroup;
  }

  /**
   * Returns the Portal object for the portal we are using. Used by MapActivity to get access to the
   * Portal object. Ideally this object would be passed in the Intent used to start MapActivity, but
   * itÕs not currently possible to serialize a Portal object for passing in an Intent.
   */
  public static Portal getPortal() {
    return mPortal;
  }

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

    // Calling setRetainInstance() causes the Fragment instance to be retained when its Activity is
    // destroyed and recreated. This allows some ArcGIS objects (Portal, PortalGroup and PortalItem)
    // to be retained so data will not need to be fetched from the network again.
    setRetainInstance(true);

    // Setup list view and list adapter
    if (mItems == null || mAdapter == null) {
      mItems = new ArrayList<PortalItemData>();
      mAdapter = new PortalItemListAdapter(getActivity(), mItems);
    }
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // Creates the view from the list_layout layout resource. The layout/list_layout.xml file
    // contains a ListView for use in portrait orientation, but layout-land/list_layout.xml contains
    // a GridView for use in landscape orientation
    View view = inflater.inflate(R.layout.list_layout, container, false);

    // Setup title
    TextView textView = (TextView) view.findViewById(R.id.listTitleTextView);
    textView.setText(R.string.itemListTitle);

    // Setup list view - maybe a ListView or a GridView
    AbsListView list = (AbsListView) view.findViewById(android.R.id.list);
    list.setAdapter(mAdapter);
    list.setOnItemClickListener(new OnItemClickListener() {

      @Override
      public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
        // Start new activity to handle the portal item the user has selected
        Intent intent = new Intent(getActivity(), MapActivity.class);
        intent.putExtra(MapActivity.KEY_PORTAL_ITEM_ID, mItems.get(position).portalItem.getItemId());
        startActivity(intent);
      }

    });

    // Setup progress dialog
    mProgressDialog = new ProgressDialog(getActivity()) {
      @Override
      public void onBackPressed() {
        // Back key pressed - dismiss the dialog and kill this fragment by popping back stack
        dismiss();
        getFragmentManager().popBackStack();
      }
    };
    return view;
  }

  @Override
  public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    if (mItems == null || mItems.size() == 0) {
      // Execute an async task to fetch and display the items
      new PortalItemsAsyncTask().execute();
    }
  }

  /**
   * This class provides an AsyncTask that fetches info about portal items from the server on a
   * background thread and displays it in a list on the UI thread.
   */
  private class PortalItemsAsyncTask extends AsyncTask<Void, Void, Void> {
    private Exception mException;

    public PortalItemsAsyncTask() {
    }

    @Override
    protected void onPreExecute() {
      // Display progress dialog on UI thread
      mProgressDialog.setOnDismissListener(new OnDismissListener() {
        @Override
        public void onDismiss(DialogInterface arg0) {
          // Cancel the task if it's not finished yet
          PortalItemsAsyncTask.this.cancel(true);
        }
      });
      mProgressDialog.setMessage(getString(R.string.fetchingItems));
      mProgressDialog.show();
    }

    @Override
    protected Void doInBackground(Void... params) {
      mException = null;
      try {
        // Do a query for all web maps in the given group
        PortalQueryParams queryParams = new PortalQueryParams();
        queryParams.setQuery(PortalItemType.WEBMAP, mPortalGroup.getGroupId(), null);
        PortalQueryResultSet<PortalItem> results = mPortal.findItems(queryParams);
        if (isCancelled()) {
          return null;
        }

        // Loop through query results
        for (PortalItem item : results.getResults()) {
          Log.d(TAG, "[item title] " + item.getTitle());

          // Fetch item thumbnail (if any) from server
          byte[] data = item.fetchThumbnail();
          if (isCancelled()) {
            return null;
          }
          if (data != null) {
            Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
            PortalItemData portalItemData = new PortalItemData(item, bitmap);
            mItems.add(portalItemData);
          }
        }

      } catch (Exception e) {
        mException = e;
      }
      return null;
    }

    @Override
    protected void onPostExecute(Void result) {
      // Display results on UI thread
      mProgressDialog.dismiss();
      if (mException != null) {
        Log.w(TAG, mException.toString());
        Toast.makeText(getActivity(), getString(R.string.fetchDataFailed), Toast.LENGTH_LONG)
            .show();
        getFragmentManager().popBackStack(); // kill this fragment
        return;
      }
      mAdapter.notifyDataSetChanged();
    }

  }

  /**
   * This class provides the adapter for the list of portal items.
   */
  private class PortalItemListAdapter extends ArrayAdapter<PortalItemData> {

    public PortalItemListAdapter(Context context, ArrayList<PortalItemData> items) {
      super(context, 0, items);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
      View view = convertView;

      // Inflate view unless we've been given an existing view to reuse
      if (convertView == null) {
        LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(
            Context.LAYOUT_INFLATER_SERVICE);
        view = inflater.inflate(R.layout.list_item, null);
      }

      // Setup item thumbnail
      PortalItemData item = getItem(position);
      ImageView image = (ImageView) view.findViewById(R.id.itemThumbnailImageView);
      image.setImageBitmap(item.itemThumbnail);

      // Setup item title
      TextView text = (TextView) view.findViewById(R.id.itemTitleTextView);
      text.setText(item.portalItem.getTitle());
      return view;
    }

  }

  /**
   * This class holds data for a portal item.
   */
  private class PortalItemData {
    PortalItem portalItem;

    Bitmap itemThumbnail;

    public PortalItemData(PortalItem item, Bitmap bt) {
      this.portalItem = item;
      this.itemThumbnail = bt;
    }
  }

}
Feedback on this topic?