FileDocCategorySizeDatePackage
LandingPage.javaAPI DocAndroid 1.5 API25914Wed May 06 22:42:48 BST 2009com.android.providers.im

LandingPage.java

/*
 * Copyright (C) 2008 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.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.providers.im;

import android.app.ListActivity;
import android.app.ActivityManagerNative;
import android.app.ActivityThread;
import android.app.Application;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.database.Cursor;
import android.im.IImPlugin;
import android.im.ImPluginConsts;
import android.im.BrandingResourceIDs;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.IBinder;
import android.provider.Im;
import android.util.Log;
import android.util.AttributeSet;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.LayoutInflater;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.CursorAdapter;

import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;

import com.android.providers.im.R;

import dalvik.system.PathClassLoader;

public class LandingPage extends ListActivity implements View.OnCreateContextMenuListener {
    private static final String TAG = "IM";
    private final static boolean LOCAL_DEBUG = false;

    private static final int ID_SIGN_IN = Menu.FIRST + 1;
    private static final int ID_SIGN_OUT = Menu.FIRST + 2;
    private static final int ID_EDIT_ACCOUNT = Menu.FIRST + 3;
    private static final int ID_REMOVE_ACCOUNT = Menu.FIRST + 4;
    private static final int ID_SIGN_OUT_ALL = Menu.FIRST + 5;
    private static final int ID_ADD_ACCOUNT = Menu.FIRST + 6;
    private static final int ID_VIEW_CONTACT_LIST = Menu.FIRST + 7;
    private static final int ID_SETTINGS = Menu.FIRST + 8;

    private ProviderAdapter mAdapter;
    private Cursor mProviderCursor;

    private static final String[] PROVIDER_PROJECTION = {
            Im.Provider._ID,
            Im.Provider.NAME,
            Im.Provider.FULLNAME,
            Im.Provider.CATEGORY,
            Im.Provider.ACTIVE_ACCOUNT_ID,
            Im.Provider.ACTIVE_ACCOUNT_USERNAME,
            Im.Provider.ACTIVE_ACCOUNT_PW,
            Im.Provider.ACTIVE_ACCOUNT_LOCKED,
            Im.Provider.ACCOUNT_PRESENCE_STATUS,
            Im.Provider.ACCOUNT_CONNECTION_STATUS,
    };

    private static final int PROVIDER_ID_COLUMN = 0;
    private static final int PROVIDER_NAME_COLUMN = 1;
    private static final int PROVIDER_FULLNAME_COLUMN = 2;
    private static final int PROVIDER_CATEGORY_COLUMN = 3;
    private static final int ACTIVE_ACCOUNT_ID_COLUMN = 4;
    private static final int ACTIVE_ACCOUNT_USERNAME_COLUMN = 5;
    private static final int ACTIVE_ACCOUNT_PW_COLUMN = 6;
    private static final int ACTIVE_ACCOUNT_LOCKED = 7;
    private static final int ACCOUNT_PRESENCE_STATUS = 8;
    private static final int ACCOUNT_CONNECTION_STATUS = 9;

    private static final String PROVIDER_SELECTION = "providers.name!=?";

    private HashMap<String, PluginInfo> mProviderToPluginMap;
    private HashMap<Long, PluginInfo> mAccountToPluginMap;
    private HashMap<Long, BrandingResources> mBrandingResources;
    private BrandingResources mDefaultBrandingResources;

    private String[] mProviderSelectionArgs = new String[1];

    public class PluginInfo {
        public IImPlugin mPlugin;
        /**
         * The name of the package that the plugin is in.
         */
        public String mPackageName;

        /**
         * The name of the class that implements {@link @ImFrontDoorPlugin} in this plugin.
         */
        public String mClassName;

        /**
         * The full path to the location of the package that the plugin is in.
         */
        public String mSrcPath;

        public PluginInfo(IImPlugin plugin, String packageName, String className,
                String srcPath) {
            mPackageName = packageName;
            mClassName = className;
            mSrcPath = srcPath;
            mPlugin = plugin;
        }
    };

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

        setTitle(R.string.landing_page_title);

        if (!loadPlugins()) {
            Log.e(TAG, "[onCreate] load plugin failed, no plugin found!");
            finish();
            return;
        }

        startPlugins();

        // get everything except for Google Talk.
        mProviderSelectionArgs[0] = Im.ProviderNames.GTALK;
        mProviderCursor = managedQuery(Im.Provider.CONTENT_URI_WITH_ACCOUNT,
                PROVIDER_PROJECTION,
                PROVIDER_SELECTION /* selection */,
                mProviderSelectionArgs /* selection args */,
                Im.Provider.DEFAULT_SORT_ORDER);
        mAdapter = new ProviderAdapter(this, mProviderCursor);
        setListAdapter(mAdapter);
        
        rebuildAccountToPluginMap();

        mBrandingResources = new HashMap<Long, BrandingResources>();
        loadDefaultBrandingRes();
        loadBrandingResources();

        registerForContextMenu(getListView());
    }

    @Override
    protected void onRestart() {
        super.onRestart();

        // refresh the accountToPlugin map after mProviderCursor is requeried
        if (!rebuildAccountToPluginMap()) {
            Log.w(TAG, "[onRestart] rebuiltAccountToPluginMap failed, reload plugins...");
            
            if (!loadPlugins()) {
                Log.e(TAG, "[onRestart] load plugin failed, no plugin found!");
                finish();
                return;
            }
            rebuildAccountToPluginMap();
        }

        startPlugins();
    }

    @Override
    protected void onStop() {
        super.onStop();
        stopPlugins();
    }

    private boolean loadPlugins() {
        mProviderToPluginMap = new HashMap<String, PluginInfo>();
        
        PackageManager pm = getPackageManager();
        List<ResolveInfo> plugins = pm.queryIntentServices(
                new Intent(ImPluginConsts.PLUGIN_ACTION_NAME),
                PackageManager.GET_META_DATA);
        for (ResolveInfo info : plugins) {
            if (Log.isLoggable(TAG, Log.DEBUG)) log("loadPlugins: found plugin " + info);

            ServiceInfo serviceInfo = info.serviceInfo;
            if (serviceInfo == null) {
                Log.e(TAG, "Ignore bad IM frontdoor plugin: " + info);
                continue;
            }

            IImPlugin plugin = null;

            // Load the plug-in directly from the apk instead of binding the service
            // and calling through the IPC binder API. It's more effective in this way
            // and we can avoid the async behaviors of binding service.
            PathClassLoader classLoader = new PathClassLoader(serviceInfo.applicationInfo.sourceDir,
                    getClassLoader());
            try {
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    log("loadPlugin: load class " + serviceInfo.name);
                }
                Class cls = classLoader.loadClass(serviceInfo.name);
                Object newInstance = cls.newInstance();
                Method m;

                // call "attach" method, so the plugin will get initialized with the proper context
                m = cls.getMethod("attach", Context.class, ActivityThread.class, String.class,
                        IBinder.class, Application.class, Object.class);
                m.invoke(newInstance,
                        new Object[] {this, null, serviceInfo.name, null, getApplication(),
                                ActivityManagerNative.getDefault()});

                // call "bind" to get the plugin object
                m = cls.getMethod("onBind", Intent.class);
                plugin = (IImPlugin)m.invoke(newInstance, new Object[]{null});
            } catch (ClassNotFoundException e) {
                Log.e(TAG, "Failed load the plugin", e);
            } catch (IllegalAccessException e) {
                Log.e(TAG, "Failed load the plugin", e);
            } catch (InstantiationException e) {
                Log.e(TAG, "Failed load the plugin", e);
            } catch (SecurityException e) {
                Log.e(TAG, "Failed load the plugin", e);
            } catch (NoSuchMethodException e) {
                Log.e(TAG, "Failed load the plugin", e);
            } catch (IllegalArgumentException e) {
                Log.e(TAG, "Failed load the plugin", e);
            } catch (InvocationTargetException e) {
                Log.e(TAG, "Failed load the plugin", e);
            }

            if (plugin != null) {
                if (Log.isLoggable(TAG, Log.DEBUG)) log("loadPlugin: plugin " + plugin + " loaded");
                ArrayList<String> providers = getSupportedProviders(plugin);

                if (providers == null || providers.size() == 0) {
                    Log.e(TAG, "Ignore bad IM frontdoor plugin: " + info + ". No providers found");
                    continue;
                }

                PluginInfo pluginInfo = new PluginInfo(plugin,
                        serviceInfo.packageName,
                        serviceInfo.name,
                        serviceInfo.applicationInfo.sourceDir);

                for (String providerName : providers) {
                    mProviderToPluginMap.put(providerName, pluginInfo);
                }
            }
        }

        return mProviderToPluginMap.size() > 0;
    }

    private void startPlugins() {
        Iterator<PluginInfo> itor = mProviderToPluginMap.values().iterator();

        while (itor.hasNext()) {
            PluginInfo pluginInfo = itor.next();
            try {
                pluginInfo.mPlugin.onStart();
            } catch (RemoteException e) {
                Log.e(TAG, "Could not start plugin " + pluginInfo.mPackageName, e);
            }
        }
    }

    private void stopPlugins() {
        Iterator<PluginInfo> itor = mProviderToPluginMap.values().iterator();

        while (itor.hasNext()) {
            PluginInfo pluginInfo = itor.next();
            try {
                pluginInfo.mPlugin.onStop();
            } catch (RemoteException e) {
                Log.e(TAG, "Could not stop plugin " + pluginInfo.mPackageName, e);
            }
        }
    }

    private ArrayList<String> getSupportedProviders(IImPlugin plugin) {
        ArrayList<String> providers = null;

        try {
            providers = (ArrayList<String>) plugin.getSupportedProviders();
        } catch (RemoteException ex) {
            Log.e(TAG, "getSupportedProviders caught ", ex);
        }

        return providers;
    }

    private void loadDefaultBrandingRes() {
        HashMap<Integer, Integer> resMapping = new HashMap<Integer, Integer>();

        resMapping.put(BrandingResourceIDs.DRAWABLE_LOGO, R.drawable.imlogo_s);
        resMapping.put(BrandingResourceIDs.DRAWABLE_PRESENCE_ONLINE,
                android.R.drawable.presence_online);
        resMapping.put(BrandingResourceIDs.DRAWABLE_PRESENCE_AWAY,
                android.R.drawable.presence_away);
        resMapping.put(BrandingResourceIDs.DRAWABLE_PRESENCE_BUSY,
                android.R.drawable.presence_busy);
        resMapping.put(BrandingResourceIDs.DRAWABLE_PRESENCE_INVISIBLE,
                android.R.drawable.presence_invisible);
        resMapping.put(BrandingResourceIDs.DRAWABLE_PRESENCE_OFFLINE,
                android.R.drawable.presence_offline);
        resMapping.put(BrandingResourceIDs.STRING_MENU_CONTACT_LIST,
                R.string.menu_view_contact_list);

        mDefaultBrandingResources = new BrandingResources(this, resMapping, null /* default res */);
    }

    private void loadBrandingResources() {
        mProviderCursor.moveToFirst();
        do {
            long providerId = mProviderCursor.getLong(PROVIDER_ID_COLUMN);
            String providerName = mProviderCursor.getString(PROVIDER_NAME_COLUMN);
            PluginInfo pluginInfo = mProviderToPluginMap.get(providerName);

            if (pluginInfo == null) {
                Log.w(TAG, "[LandingPage] loadBrandingResources: no plugin found for " + providerName);
                continue;
            }
            
            if (!mBrandingResources.containsKey(providerId)) {
                BrandingResources res = new BrandingResources(this, pluginInfo, providerName,
                        mDefaultBrandingResources);
                mBrandingResources.put(providerId, res);
            }
        } while (mProviderCursor.moveToNext()) ;
    }

    public BrandingResources getBrandingResource(long providerId) {
        BrandingResources res = mBrandingResources.get(providerId);
        return res == null ? mDefaultBrandingResources : res;
    }

    private boolean rebuildAccountToPluginMap() {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            log("rebuildAccountToPluginMap");
        }
        
        if (mAccountToPluginMap != null) {
            mAccountToPluginMap.clear();
        }
        
        mAccountToPluginMap = new HashMap<Long, PluginInfo>();

        mProviderCursor.moveToFirst();

        boolean retVal = true;

        do {
            long accountId = mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN);

            if (accountId == 0) {
                continue;
            }
            
            String name = mProviderCursor.getString(PROVIDER_NAME_COLUMN);
            PluginInfo pluginInfo = mProviderToPluginMap.get(name);
            if (pluginInfo != null) {
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    log("rebuildAccountToPluginMap: add plugin for acct=" + accountId + ", provider=" + name);
                }
                mAccountToPluginMap.put(accountId, pluginInfo);
            } else {
                Log.w(TAG, "[LandingPage] no plugin found for " + name);
                retVal = false;
            }
        } while (mProviderCursor.moveToNext()) ;

        return retVal;
    }

    private void signIn(long accountId) {
        if (accountId == 0) {
            Log.w(TAG, "signIn: account id is 0, bail");
            return;
        }

        boolean isAccountEditible = mProviderCursor.getInt(ACTIVE_ACCOUNT_LOCKED) == 0;
        if (isAccountEditible && mProviderCursor.isNull(ACTIVE_ACCOUNT_PW_COLUMN)) {
            // no password, edit the account
            if (Log.isLoggable(TAG, Log.DEBUG)) log("no pw for account " + accountId);
            Intent intent = getEditAccountIntent();
            startActivity(intent);
            return;
        }


        PluginInfo pluginInfo = mAccountToPluginMap.get(accountId);
        if (pluginInfo == null) {
            Log.e(TAG, "signIn: cannot find plugin for account " + accountId);
            return;
        }

        try {
            if (Log.isLoggable(TAG, Log.DEBUG)) log("sign in for account " + accountId);
            pluginInfo.mPlugin.signIn(accountId);
        } catch (RemoteException ex) {
            Log.e(TAG, "signIn failed", ex);
        }
    }

    boolean isSigningIn(Cursor cursor) {
        int connectionStatus = cursor.getInt(ACCOUNT_CONNECTION_STATUS);
        return connectionStatus == Im.ConnectionStatus.CONNECTING;
    }

    boolean isSignedIn(Cursor cursor) {
        int connectionStatus = cursor.getInt(ACCOUNT_CONNECTION_STATUS);
        return connectionStatus == Im.ConnectionStatus.ONLINE;
    }

    private boolean allAccountsSignedOut() {
        mProviderCursor.moveToFirst();
        do {
            if (isSignedIn(mProviderCursor)) {
                return false;
            }
        } while (mProviderCursor.moveToNext()) ;

        return true;
    }

    private void signoutAll() {
        do {
            long accountId = mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN);
            signOut(accountId);
        } while (mProviderCursor.moveToNext()) ;
    }

    private void signOut(long accountId) {
        if (accountId == 0) {
            Log.w(TAG, "signOut: account id is 0, bail");
            return;
        }

        PluginInfo pluginInfo = mAccountToPluginMap.get(accountId);
        if (pluginInfo == null) {
            Log.e(TAG, "signOut: cannot find plugin for account " + accountId);
            return;
        }

        try {
            if (Log.isLoggable(TAG, Log.DEBUG)) log("sign out for account " + accountId);
            pluginInfo.mPlugin.signOut(accountId);
        } catch (RemoteException ex) {
            Log.e(TAG, "signOut failed", ex);
        }
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        super.onPrepareOptionsMenu(menu);
        menu.findItem(ID_SIGN_OUT_ALL).setVisible(!allAccountsSignedOut());
        return true;
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        menu.add(0, ID_SIGN_OUT_ALL, 0, R.string.menu_sign_out_all)
                .setIcon(android.R.drawable.ic_menu_close_clear_cancel);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case ID_SIGN_OUT_ALL:
                signoutAll();
                return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
        AdapterView.AdapterContextMenuInfo info;
        try {
            info = (AdapterView.AdapterContextMenuInfo) menuInfo;
        } catch (ClassCastException e) {
            Log.e(TAG, "bad menuInfo", e);
            return;
        }

        Cursor providerCursor = (Cursor) getListAdapter().getItem(info.position);
        menu.setHeaderTitle(providerCursor.getString(PROVIDER_FULLNAME_COLUMN));

        if (providerCursor.isNull(ACTIVE_ACCOUNT_ID_COLUMN)) {
            menu.add(0, ID_ADD_ACCOUNT, 0, R.string.menu_add_account);
            return;
        }

        long providerId = providerCursor.getLong(PROVIDER_ID_COLUMN);
        boolean isLoggingIn = isSigningIn(providerCursor);
        boolean isLoggedIn = isSignedIn(providerCursor);

        if (!isLoggedIn) {
            menu.add(0, ID_SIGN_IN, 0, R.string.sign_in).setIcon(com.android.internal.R.drawable.ic_menu_login);
        } else {
            BrandingResources brandingRes = getBrandingResource(providerId);
            menu.add(0, ID_VIEW_CONTACT_LIST, 0,
                    brandingRes.getString(BrandingResourceIDs.STRING_MENU_CONTACT_LIST));
            menu.add(0, ID_SIGN_OUT, 0, R.string.menu_sign_out)
                .setIcon(android.R.drawable.ic_menu_close_clear_cancel);
        }

        boolean isAccountEditible = providerCursor.getInt(ACTIVE_ACCOUNT_LOCKED) == 0;
        if (isAccountEditible && !isLoggingIn && !isLoggedIn) {
            menu.add(0, ID_EDIT_ACCOUNT, 0, R.string.menu_edit_account)
                .setIcon(android.R.drawable.ic_menu_edit);
            menu.add(0, ID_REMOVE_ACCOUNT, 0, R.string.menu_remove_account)
                .setIcon(android.R.drawable.ic_menu_delete);
        }

        // always add a settings menu item
        menu.add(0, ID_SETTINGS, 0, R.string.menu_settings);
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        AdapterView.AdapterContextMenuInfo info;
        try {
            info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
        } catch (ClassCastException e) {
            Log.e(TAG, "bad menuInfo", e);
            return false;
        }
        long providerId = info.id;
        Cursor providerCursor = (Cursor) getListAdapter().getItem(info.position);
        long accountId = providerCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN);

        switch (item.getItemId()) {
            case ID_EDIT_ACCOUNT:
            {
                startActivity(getEditAccountIntent());
                return true;
            }

            case ID_REMOVE_ACCOUNT:
            {
                Uri accountUri = ContentUris.withAppendedId(Im.Account.CONTENT_URI, accountId);
                getContentResolver().delete(accountUri, null, null);
                // Requery the cursor to force refreshing screen
                providerCursor.requery();
                return true;
            }

            case ID_VIEW_CONTACT_LIST:
            {
                Intent intent = getViewContactsIntent();
                startActivity(intent);
                return true;
            }
            case ID_ADD_ACCOUNT:
            {
                startActivity(getCreateAccountIntent());
                return true;
            }

            case ID_SIGN_IN:
            {
                signIn(accountId);
                return true;
            }

            case ID_SIGN_OUT:
            {
                // TODO: progress bar
                signOut(accountId);
                return true;
            }

            case ID_SETTINGS:
            {
                Intent intent = new Intent(Intent.ACTION_VIEW, Im.ProviderSettings.CONTENT_URI);
                intent.addCategory(getProviderCategory(providerCursor));
                intent.putExtra("providerId", providerId);
                startActivity(intent);
                return true;
            }

        }

        return false;
    }

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        Intent intent = null;
        mProviderCursor.moveToPosition(position);

        if (mProviderCursor.isNull(ACTIVE_ACCOUNT_ID_COLUMN)) {
            // add account
            intent = getCreateAccountIntent();
        } else {
            int state = mProviderCursor.getInt(ACCOUNT_CONNECTION_STATUS);

            if (state == Im.ConnectionStatus.OFFLINE || state == Im.ConnectionStatus.CONNECTING) {
                boolean isAccountEditible = mProviderCursor.getInt(ACTIVE_ACCOUNT_LOCKED) == 0;
                if (isAccountEditible && mProviderCursor.isNull(ACTIVE_ACCOUNT_PW_COLUMN)) {
                    // no password, edit the account
                    intent = getEditAccountIntent();
                } else {
                    long accountId = mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN);
                    signIn(accountId);
                }
            } else {
                intent = getViewContactsIntent();
            }
        }

        if (intent != null) {
            startActivity(intent);
        }
    }

    Intent getCreateAccountIntent() {
        Intent intent = new Intent();
        intent.setAction(Intent.ACTION_INSERT);

        long providerId = mProviderCursor.getLong(PROVIDER_ID_COLUMN);
        intent.setData(ContentUris.withAppendedId(Im.Provider.CONTENT_URI, providerId));
        intent.addCategory(getProviderCategory(mProviderCursor));
        return intent;
    }

    Intent getEditAccountIntent() {
        Intent intent = new Intent(Intent.ACTION_EDIT,
                ContentUris.withAppendedId(Im.Account.CONTENT_URI,
                        mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN)));
        intent.addCategory(getProviderCategory(mProviderCursor));
        return intent;
    }

    Intent getViewContactsIntent() {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setData(Im.Contacts.CONTENT_URI);
        intent.addCategory(getProviderCategory(mProviderCursor));
        intent.putExtra("accountId", mProviderCursor.getLong(ACTIVE_ACCOUNT_ID_COLUMN));
        return intent;
    }

    private String getProviderCategory(Cursor cursor) {
        return cursor.getString(PROVIDER_CATEGORY_COLUMN);
    }

    
    static void log(String msg) {
        Log.d(TAG, "[LandingPage]" + msg);
    }

    private class ProviderListItemFactory implements LayoutInflater.Factory {
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            if (name != null && name.equals(ProviderListItem.class.getName())) {
                return new ProviderListItem(context, LandingPage.this);
            }
            return null;
        }
    }

    private final class ProviderAdapter extends CursorAdapter {
        private LayoutInflater mInflater;

        public ProviderAdapter(Context context, Cursor c) {
            super(context, c);
            mInflater = LayoutInflater.from(context).cloneInContext(context);
            mInflater.setFactory(new ProviderListItemFactory());
        }

        @Override
        public View newView(Context context, Cursor cursor, ViewGroup parent) {
            // create a custom view, so we can manage it ourselves. Mainly, we want to
            // initialize the widget views (by calling getViewById()) in newView() instead of in
            // bindView(), which can be called more often.
            ProviderListItem view = (ProviderListItem) mInflater.inflate(
                    R.layout.account_view, parent, false);
            view.init(cursor);
            return view;
        }

        @Override
        public void bindView(View view, Context context, Cursor cursor) {
            ((ProviderListItem) view).bindView(cursor);
        }
    }

}