Show tab icons in view activity.

-Add an asynchronous request api to Sources, to allow for
asynchronous binding of autheticator data to the Sources object. The old
getInstance() api has been changed to getPartialInstance(), as not all
clients of Sources need the authenticator data bound.

-BaseContactProvider uses Sources to get the source icons. This behavior
still needs to be added to EditContactActivity.
diff --git a/src/com/android/contacts/BaseContactCardActivity.java b/src/com/android/contacts/BaseContactCardActivity.java
index 7d4bb19..b4980f6 100644
--- a/src/com/android/contacts/BaseContactCardActivity.java
+++ b/src/com/android/contacts/BaseContactCardActivity.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008 The Android Open Source Project
+ * Copyright (C) 2009 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,32 +16,33 @@
 
 package com.android.contacts;
 
+import java.util.ArrayList;
+
+import com.android.contacts.NotifyingAsyncQueryHandler.AsyncQueryListener;
 import com.android.contacts.ScrollingTabWidget.OnTabSelectionChangedListener;
-import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.Sources;
 import com.android.internal.widget.ContactHeaderWidget;
 
 import android.app.Activity;
 import android.content.ContentUris;
-import android.content.ContentValues;
 import android.content.Context;
+import android.content.Entity;
+import android.content.EntityIterator;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.database.Cursor;
-import android.graphics.Bitmap;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.SystemClock;
-import android.provider.SocialContract;
-import android.provider.ContactsContract.Contacts;
+import android.os.RemoteException;
 import android.provider.ContactsContract.RawContacts;
-import android.provider.SocialContract.Activities;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.Window;
-import android.widget.CheckBox;
 import android.widget.ImageView;
 import android.widget.TextView;
 
@@ -49,7 +50,8 @@
  * The base Activity class for viewing and editing a contact.
  */
 public abstract class BaseContactCardActivity extends Activity
-        implements QueryCompleteListener, OnTabSelectionChangedListener {
+        implements AsyncQueryListener, OnTabSelectionChangedListener,
+        Sources.SourcesCompleteListener {
 
     private static final String TAG = "BaseContactCardActivity";
 
@@ -73,6 +75,8 @@
 
     private static final int TOKEN_TABS = 0;
 
+    private Sources mSources;
+
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
@@ -91,17 +95,24 @@
         mTabWidget = (ScrollingTabWidget) findViewById(R.id.tab_widget);
 
         mTabWidget.setTabSelectionListener(this);
+        mTabWidget.setVisibility(View.INVISIBLE);
         mTabRawContactIdMap = new SparseArray<Long>();
 
         mHandler = new NotifyingAsyncQueryHandler(this, this);
 
-        setupTabs();
+        Sources.requestInstance(this, this);
+
     }
 
-    private void setupTabs() {
+    public void onSourcesComplete(Sources sources) {
+        mSources = sources;
+        asyncSetupTabs();
+    }
+
+    private void asyncSetupTabs() {
         long contactId = ContentUris.parseId(mUri);
-        mHandler.startQuery(TOKEN_TABS, null, RawContacts.CONTENT_URI, TAB_PROJECTION,
-                RawContacts.CONTACT_ID + "=" + contactId, null, null);
+        mHandler.startQueryEntities(TOKEN_TABS, null,
+                RawContacts.CONTENT_URI, RawContacts.CONTACT_ID + "=" + contactId, null, null);
     }
 
     /**
@@ -115,41 +126,59 @@
     }
 
     /** {@inheritDoc} */
-    public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+    public void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
         try{
             if (token == TOKEN_TABS) {
                 clearCurrentTabs();
-                bindTabs(cursor);
+                bindTabs(readEntities(iterator));
             }
         } finally {
-            if (cursor != null) {
-                cursor.close();
+            if (iterator != null) {
+                iterator.close();
             }
         }
     }
 
+    /** {@inheritDoc} */
+    public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+        // Emtpy
+    }
+
+    private ArrayList<Entity> readEntities(EntityIterator iterator) {
+        ArrayList<Entity> entities = new ArrayList<Entity>();
+        try {
+            while (iterator.hasNext()) {
+                entities.add(iterator.next());
+            }
+        } catch (RemoteException e) {
+        }
+
+        return entities;
+    }
+
     /**
      * Adds a tab for each {@link RawContact} associated with this contact.
      * Override this method if you want to additional tabs and/or different
      * tabs for your activity.
      *
-     * @param tabsCursor A cursor over all the RawContacts associated with
-     * the contact being displayed. Use {@link TAB_CONTACT_ID_COLUMN_INDEX},
-     * {@link TAB_ACCOUNT_NAME_COLUMN_INDEX}, {@link TAB_ACCOUNT_TYPE_COLUMN_INDEX},
-     * and {@link TAB_PACKAGE_COLUMN_INDEX} as column indexes on the cursor.
+     * @param entities An {@link ArrayList} of {@link Entity}s of all the RawContacts
+     * associated with the contact being displayed.
      */
-    protected void bindTabs(Cursor tabsCursor) {
-        while (tabsCursor.moveToNext()) {
-            long contactId = tabsCursor.getLong(TAB_CONTACT_ID_COLUMN_INDEX);
-
-            //TODO: figure out how to get the icon
-            Drawable tabIcon = null;
-            addTab(contactId, null, tabIcon);
+    protected void bindTabs(ArrayList<Entity> entities) {
+        for (Entity entity : entities) {
+            final String accountType = entity.getEntityValues().
+                    getAsString(RawContacts.ACCOUNT_TYPE);
+            final Long rawContactId = entity.getEntityValues().
+                    getAsLong(RawContacts._ID);
+            ContactsSource source = mSources.getSourceForType(accountType);
+            addTab(rawContactId, createTabIndicatorView(mTabWidget, source));
         }
-        selectDefaultTab();
-
+        mTabWidget.setCurrentTab(0);
+        mTabWidget.setVisibility(View.VISIBLE);
+        mTabWidget.postInvalidate();
     }
 
+
     /**
      * Add a tab to be displayed in the {@link ScrollingTabWidget}.
      *
@@ -167,8 +196,8 @@
      * @param contactId The contact id associated with the tab.
      * @param view A view to use as the tab indicator.
      */
-    protected void addTab(long contactId, View view) {
-        mTabRawContactIdMap.put(mTabWidget.getTabCount(), contactId);
+    protected void addTab(long rawContactId, View view) {
+        mTabRawContactIdMap.put(mTabWidget.getTabCount(), rawContactId);
         mTabWidget.addTab(view);
     }
 
@@ -198,6 +227,7 @@
     /**
      * Utility for creating a standard tab indicator view.
      *
+     * @param parent The parent ViewGroup to attach the new view to.
      * @param label The label to display in the tab indicator. If null, not label will be displayed.
      * @param icon The icon to display. If null, no icon will be displayed.
      * @return The tab indicator View.
@@ -216,4 +246,30 @@
         return tabIndicator;
     }
 
+    /**
+     * Utility for creating a standard tab indicator view.
+     *
+     * @param context The label to display in the tab indicator. If null, not label will be displayed.
+     * @param parent The parent ViewGroup to attach the new view to.
+     * @param source The {@link ContactsSource} to build the tab view from.
+     * @return The tab indicator View.
+     */
+    public static View createTabIndicatorView(ViewGroup parent, ContactsSource source) {
+        Drawable icon = null;
+        if (source != null) {
+            final String packageName = source.resPackageName;
+            if (source.iconRes > 0) {
+                try {
+                    final Context authContext = parent.getContext().
+                            createPackageContext(packageName, 0);
+                    icon = authContext.getResources().getDrawable(source.iconRes);
+
+                } catch (PackageManager.NameNotFoundException e) {
+                    Log.d(TAG, "error getting the Package Context for " + packageName, e);
+                }
+            }
+        }
+        return createTabIndicatorView(parent, null, icon);
+    }
+
 }
diff --git a/src/com/android/contacts/DisplayGroupsActivity.java b/src/com/android/contacts/DisplayGroupsActivity.java
index 269ed0b..6da2531 100644
--- a/src/com/android/contacts/DisplayGroupsActivity.java
+++ b/src/com/android/contacts/DisplayGroupsActivity.java
@@ -16,13 +16,14 @@
 
 package com.android.contacts;
 
-import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener;
+import com.android.contacts.NotifyingAsyncQueryHandler.AsyncQueryListener;
 
 import android.app.ExpandableListActivity;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.EntityIterator;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.Editor;
 import android.content.pm.PackageManager;
@@ -59,7 +60,7 @@
  * select which ones they want to be visible.
  */
 public final class DisplayGroupsActivity extends ExpandableListActivity implements
-        QueryCompleteListener, OnItemClickListener {
+        AsyncQueryListener, OnItemClickListener {
     private static final String TAG = "DisplayGroupsActivity";
 
     public interface Prefs {
@@ -631,4 +632,8 @@
 
     }
 
+    public void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
+        // Emtpy
+    }
+
 }
diff --git a/src/com/android/contacts/NotifyingAsyncQueryHandler.java b/src/com/android/contacts/NotifyingAsyncQueryHandler.java
index 2223e9c..0eff1ac 100644
--- a/src/com/android/contacts/NotifyingAsyncQueryHandler.java
+++ b/src/com/android/contacts/NotifyingAsyncQueryHandler.java
@@ -2,6 +2,7 @@
 
 import android.content.AsyncQueryHandler;
 import android.content.Context;
+import android.content.EntityIterator;
 import android.database.Cursor;
 
 import java.lang.ref.WeakReference;
@@ -14,28 +15,48 @@
  * Using this pattern will help keep you from leaking a {@link Context}.
  */
 public class NotifyingAsyncQueryHandler extends AsyncQueryHandler {
-    private final WeakReference<QueryCompleteListener> mListener;
+    private WeakReference<AsyncQueryListener> mListener;
 
     /**
-     * Interface to listen for completed queries.
+     * Interface to listen for completed query operations.
      */
-    public static interface QueryCompleteListener {
-        public void onQueryComplete(int token, Object cookie, Cursor cursor);
+    public interface AsyncQueryListener {
+        void onQueryComplete(int token, Object cookie, Cursor cursor);
+        void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator);
     }
 
-    public NotifyingAsyncQueryHandler(Context context, QueryCompleteListener listener) {
+    public NotifyingAsyncQueryHandler(Context context, AsyncQueryListener listener) {
         super(context.getContentResolver());
-        mListener = new WeakReference<QueryCompleteListener>(listener);
+        setQueryListener(listener);
+    }
+
+    /**
+     * Assign the given {@link AsyncQueryListener} to receive query events from
+     * asynchronous calls. Will replace any existing listener.
+     */
+    public void setQueryListener(AsyncQueryListener listener) {
+        mListener = new WeakReference<AsyncQueryListener>(listener);
     }
 
     /** {@inheritDoc} */
     @Override
     protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
-        final QueryCompleteListener listener = mListener.get();
+        final AsyncQueryListener listener = mListener.get();
         if (listener != null) {
             listener.onQueryComplete(token, cookie, cursor);
-        } else {
+        } else if (cursor != null) {
             cursor.close();
         }
     }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
+        final AsyncQueryListener listener = mListener.get();
+        if (listener != null) {
+            listener.onQueryEntitiesComplete(token, cookie, iterator);
+        } else if (iterator != null) {
+            iterator.close();
+        }
+    }
 }
diff --git a/src/com/android/contacts/ShowOrCreateActivity.java b/src/com/android/contacts/ShowOrCreateActivity.java
index fb6f60b..6ba2315 100755
--- a/src/com/android/contacts/ShowOrCreateActivity.java
+++ b/src/com/android/contacts/ShowOrCreateActivity.java
@@ -16,7 +16,7 @@
 
 package com.android.contacts;
 
-import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener;
+import com.android.contacts.NotifyingAsyncQueryHandler.AsyncQueryListener;
 import com.android.contacts.ui.FastTrackWindow;
 
 import android.app.Activity;
@@ -25,6 +25,7 @@
 import android.content.ContentUris;
 import android.content.Context;
 import android.content.DialogInterface;
+import android.content.EntityIterator;
 import android.content.Intent;
 import android.database.Cursor;
 import android.graphics.Rect;
@@ -53,7 +54,7 @@
  * {@link Intent#ACTION_SEARCH}.
  * </ul>
  */
-public final class ShowOrCreateActivity extends Activity implements QueryCompleteListener,
+public final class ShowOrCreateActivity extends Activity implements AsyncQueryListener,
         FastTrackWindow.OnDismissListener {
     static final String TAG = "ShowOrCreateActivity";
     static final boolean LOGD = false;
@@ -267,4 +268,8 @@
             mParent.finish();
         }
     }
+
+    public void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
+        // Empty
+    }
 }
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index ec46690..87e6718 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -44,6 +44,7 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.DialogInterface;
+import android.content.Entity;
 import android.content.Intent;
 import android.content.DialogInterface.OnClickListener;
 import android.content.pm.PackageManager;
@@ -252,11 +253,11 @@
     }
 
     @Override
-    protected void bindTabs(Cursor tabsCursor) {
-        if (tabsCursor.getCount() > 1) {
+    protected void bindTabs(ArrayList<Entity> entities) {
+        if (entities.size() > 1) {
             addAllTab();
         }
-        super.bindTabs(tabsCursor);
+        super.bindTabs(entities);
     }
 
     private void addAllTab() {
diff --git a/src/com/android/contacts/model/EntityModifier.java b/src/com/android/contacts/model/EntityModifier.java
index de925cb..ed37736 100644
--- a/src/com/android/contacts/model/EntityModifier.java
+++ b/src/com/android/contacts/model/EntityModifier.java
@@ -299,7 +299,8 @@
      */
     public static void parseExtras(Context context, EntityDelta state, Bundle extras) {
         final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
-        final ContactsSource source = Sources.getInstance(context).getSourceForType(accountType);
+        final ContactsSource source = Sources.getPartialInstance(context).
+                getSourceForType(accountType);
 
         {
             // StructuredName
diff --git a/src/com/android/contacts/model/Sources.java b/src/com/android/contacts/model/Sources.java
index f9618b2..8f09454 100644
--- a/src/com/android/contacts/model/Sources.java
+++ b/src/com/android/contacts/model/Sources.java
@@ -18,6 +18,11 @@
 
 import com.android.contacts.R;
 
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorDescription;
+import android.accounts.Future1;
+import android.accounts.Future1Callback;
+import android.accounts.OperationCanceledException;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -34,6 +39,7 @@
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.util.Log;
 import android.view.inputmethod.EditorInfo;
 
 import com.android.contacts.model.ContactsSource.DataKind;
@@ -41,6 +47,7 @@
 import com.android.contacts.model.ContactsSource.EditField;
 import com.android.contacts.model.ContactsSource.StringInflater;
 
+import java.lang.ref.SoftReference;
 import java.util.ArrayList;
 import java.util.HashMap;
 
@@ -54,23 +61,67 @@
 public class Sources {
     // TODO: finish hard-coding all constraints
 
-    private static Sources sInstance;
+    private static SoftReference<Sources> sInstance;
 
-    public static synchronized Sources getInstance(Context context) {
-        if (sInstance == null) {
-            sInstance = new Sources(context);
+    private ArrayList<SourcesCompleteListener> mSourcesCompleteListeners;
+    private boolean mComplete = false;
+
+    /**
+     * Returns the singleton {@link Sources} without binding data from
+     * the available authenticators. All clients of this class should move
+     * to requestInstance().
+     */
+    @Deprecated
+    public static synchronized Sources getPartialInstance(Context context) {
+        if (sInstance == null || sInstance.get() == null) {
+            sInstance = new SoftReference<Sources>(new Sources(context, false));
         }
-        return sInstance;
+        return sInstance.get();
+    }
+
+    /**
+     * Requests the singleton instance of {@link Sources} with data bound
+     * from the available authenticators. The result will be returned to
+     * the {@link SourcesCompleteListener} callback interface.
+     * @param context
+     * @param listener An implementation of {@link SourcesCompleteListener} to
+     * pass the {@link Sources} object to.
+     */
+    public static synchronized void requestInstance(Context context,
+            SourcesCompleteListener listener) {
+        Sources sources = sInstance == null ? null : sInstance.get();
+        if (sources == null) {
+            sources = new Sources(context, true);
+            sources.mSourcesCompleteListeners.add(listener);
+            sInstance = new SoftReference<Sources>(sources);
+        } else {
+            if (sources.mComplete) {
+                // We're already complete, so we can call the callback right now.
+                listener.onSourcesComplete(sources);
+            } else {
+                sources.mSourcesCompleteListeners.add(listener);
+            }
+        }
+
     }
 
     public static final String ACCOUNT_TYPE_GOOGLE = "com.google.GAIA";
     public static final String ACCOUNT_TYPE_EXCHANGE = "com.android.exchange";
+    public static final String ACCOUNT_TYPE_FACEBOOK = "com.facebook.auth.login";
 
     private HashMap<String, ContactsSource> mSources = new HashMap<String, ContactsSource>();
+    private AccountManager mAccountManager;
 
-    private Sources(Context context) {
+    private Sources(Context context, boolean fetchAuthenticatorData) {
         mSources.put(ACCOUNT_TYPE_GOOGLE, buildGoogle(context));
         mSources.put(ACCOUNT_TYPE_EXCHANGE, buildExchange(context));
+        mSources.put(ACCOUNT_TYPE_FACEBOOK, buildFacebook(context));
+
+        if (fetchAuthenticatorData) {
+            mSourcesCompleteListeners = new ArrayList<SourcesCompleteListener>();
+            mAccountManager = AccountManager.get(context);
+            asyncGetAuthenticatorTypes();
+        }
     }
 
     /**
@@ -483,6 +534,18 @@
     }
 
     /**
+     * Hard-coded instance of {@link ContactsSource} for Facebook.
+     */
+    private ContactsSource buildFacebook(Context context) {
+        final ContactsSource list = new ContactsSource();
+        list.accountType = ACCOUNT_TYPE_FACEBOOK;
+        list.resPackageName = context.getPackageName();
+
+        return list;
+
+    }
+
+    /**
      * Simple inflater that assumes a string resource has a "%s" that will be
      * filled from the given column.
      */
@@ -564,4 +627,41 @@
                     type.actionAltRes, null) : null;
         }
     }
+
+    private void asyncGetAuthenticatorTypes() {
+        mAccountManager.getAuthenticatorTypes(new GetAuthenticatorsCallback(), null /* handler */);
+    }
+
+    private class GetAuthenticatorsCallback implements Future1Callback<AuthenticatorDescription[]> {
+        public void run(Future1<AuthenticatorDescription[]> future) {
+            try {
+                AuthenticatorDescription[] authenticatorDescs = future.getResult();
+
+                for (int i = 0; i < authenticatorDescs.length; i++) {
+                    String accountType = authenticatorDescs[i].type;
+                    ContactsSource contactSource = mSources.get(accountType);
+                    if (contactSource != null) {
+                        contactSource.iconRes = authenticatorDescs[i].iconId;
+                        contactSource.titleRes = authenticatorDescs[i].labelId;
+                        contactSource.resPackageName = authenticatorDescs[i].packageName;;
+                    }
+                }
+            } catch (OperationCanceledException e) {
+                // the request was canceled
+            }
+
+            mComplete = true;
+            for (SourcesCompleteListener listener : mSourcesCompleteListeners) {
+                listener.onSourcesComplete(Sources.this);
+            }
+        }
+    }
+
+    /**
+     * Callback interface used for being notified when the Sources object
+     * has finished binding with data from the Authenticators.
+     */
+    public interface SourcesCompleteListener {
+        public void onSourcesComplete(Sources sources);
+    }
 }
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index 8b4442e..ad2c9ed 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -99,7 +99,7 @@
         final Bundle extras = intent.getExtras();
 
         mUri = intent.getData();
-        mSources = Sources.getInstance(this);
+        mSources = Sources.getPartialInstance(this);
 
         setContentView(R.layout.act_edit);
 
diff --git a/src/com/android/contacts/ui/FastTrackWindow.java b/src/com/android/contacts/ui/FastTrackWindow.java
index 4a2acbc..9f4347e 100644
--- a/src/com/android/contacts/ui/FastTrackWindow.java
+++ b/src/com/android/contacts/ui/FastTrackWindow.java
@@ -17,7 +17,7 @@
 
 import com.android.contacts.NotifyingAsyncQueryHandler;
 import com.android.contacts.R;
-import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener;
+import com.android.contacts.NotifyingAsyncQueryHandler.AsyncQueryListener;
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.Sources;
 import com.android.contacts.model.ContactsSource.DataKind;
@@ -26,6 +26,7 @@
 import android.content.ActivityNotFoundException;
 import android.content.ContentUris;
 import android.content.Context;
+import android.content.EntityIterator;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.database.Cursor;
@@ -83,7 +84,7 @@
  * Window that shows fast-track contact details for a specific
  * {@link Contacts#_ID}.
  */
-public class FastTrackWindow implements Window.Callback, QueryCompleteListener, OnClickListener,
+public class FastTrackWindow implements Window.Callback, AsyncQueryListener, OnClickListener,
         AbsListView.OnItemClickListener {
     private static final String TAG = "FastTrackWindow";
 
@@ -709,7 +710,7 @@
     private void handleData(Cursor cursor) {
         if (cursor == null) return;
 
-        final ContactsSource defaultSource = Sources.getInstance(mContext).getSourceForType(
+        final ContactsSource defaultSource = Sources.getPartialInstance(mContext).getSourceForType(
                 Sources.ACCOUNT_TYPE_GOOGLE);
 
         {
@@ -997,4 +998,8 @@
     /** {@inheritDoc} */
     public void onWindowFocusChanged(boolean hasFocus) {
     }
+
+    public void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
+        //Empty
+    }
 }