Project: android-thaiime
/*
 * Copyright (C) 2010 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.common.contacts; 
 
import com.android.common.widget.CompositeCursorAdapter; 
 
import android.accounts.Account; 
import android.content.ContentResolver; 
import android.content.Context; 
import android.content.pm.PackageManager; 
import android.content.pm.PackageManager.NameNotFoundException; 
import android.content.res.Resources; 
import android.database.Cursor; 
import android.database.MatrixCursor; 
import android.net.Uri; 
import android.os.Handler; 
import android.os.Message; 
import android.provider.ContactsContract; 
import android.provider.ContactsContract.CommonDataKinds.Email; 
import android.provider.ContactsContract.Contacts; 
import android.text.TextUtils; 
import android.text.util.Rfc822Token; 
import android.util.Log; 
import android.view.View; 
import android.view.ViewGroup; 
import android.widget.Filter; 
import android.widget.Filterable; 
 
import java.util.ArrayList; 
import java.util.List; 
 
/**
 * A base class for email address autocomplete adapters. It uses 
 * {@link Email#CONTENT_FILTER_URI} to search for data rows by email address 
 * and/or contact name. It also searches registered {@link Directory}'s. 
 */
 
public abstract class BaseEmailAddressAdapter extends CompositeCursorAdapter implements Filterable { 
 
    private static final String TAG = "BaseEmailAddressAdapter"
 
    // TODO: revert to references to the Directory class as soon as the 
    // issue with the dependency on SDK 8 is resolved 
 
    // This is Directory.LOCAL_INVISIBLE 
    private static final long DIRECTORY_LOCAL_INVISIBLE = 1
 
    // This is ContactsContract.DIRECTORY_PARAM_KEY 
    private static final String DIRECTORY_PARAM_KEY = "directory"
 
    // This is ContactsContract.LIMIT_PARAM_KEY 
    private static final String LIMIT_PARAM_KEY = "limit"
 
    // This is ContactsContract.PRIMARY_ACCOUNT_NAME 
    private static final String PRIMARY_ACCOUNT_NAME = "name_for_primary_account"
    // This is ContactsContract.PRIMARY_ACCOUNT_TYPE 
    private static final String PRIMARY_ACCOUNT_TYPE = "type_for_primary_account"
 
    /**
     * The preferred number of results to be retrieved. This number may be 
     * exceeded if there are several directories configured, because we will use 
     * the same limit for all directories. 
     */
 
    private static final int DEFAULT_PREFERRED_MAX_RESULT_COUNT = 10
 
    /**
     * The number of extra entries requested to allow for duplicates. Duplicates 
     * are removed from the overall result. 
     */
 
    private static final int ALLOWANCE_FOR_DUPLICATES = 5
 
    /**
     * The "Searching..." message will be displayed if search is not complete 
     * within this many milliseconds. 
     */
 
    private static final int MESSAGE_SEARCH_PENDING_DELAY = 1000
 
    private static final int MESSAGE_SEARCH_PENDING = 1
 
    /**
     * Model object for a {@link Directory} row. There is a partition in the 
     * {@link CompositeCursorAdapter} for every directory (except 
     * {@link Directory#LOCAL_INVISIBLE}. 
     */
 
    public final static class DirectoryPartition extends CompositeCursorAdapter.Partition { 
        public long directoryId; 
        public String directoryType; 
        public String displayName; 
        public String accountName; 
        public String accountType; 
        public boolean loading; 
        public CharSequence constraint; 
        public DirectoryPartitionFilter filter; 
 
        public DirectoryPartition() { 
            super(falsefalse); 
        } 
    } 
 
    private static class EmailQuery { 
        public static final String[] PROJECTION = { 
            Contacts.DISPLAY_NAME,  // 0 
            Email.DATA              // 1 
        }; 
 
        public static final int NAME = 0
        public static final int ADDRESS = 1
    } 
 
    private static class DirectoryListQuery { 
 
        // TODO: revert to references to the Directory class as soon as the 
        // issue with the dependency on SDK 8 is resolved 
        public static final Uri URI = 
                Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories"); 
        private static final String DIRECTORY_ID = "_id"
        private static final String DIRECTORY_ACCOUNT_NAME = "accountName"
        private static final String DIRECTORY_ACCOUNT_TYPE = "accountType"
        private static final String DIRECTORY_DISPLAY_NAME = "displayName"
        private static final String DIRECTORY_PACKAGE_NAME = "packageName"
        private static final String DIRECTORY_TYPE_RESOURCE_ID = "typeResourceId"
 
        public static final String[] PROJECTION = { 
            DIRECTORY_ID,               // 0 
            DIRECTORY_ACCOUNT_NAME,     // 1 
            DIRECTORY_ACCOUNT_TYPE,     // 2 
            DIRECTORY_DISPLAY_NAME,     // 3 
            DIRECTORY_PACKAGE_NAME,     // 4 
            DIRECTORY_TYPE_RESOURCE_ID, // 5 
        }; 
 
        public static final int ID = 0
        public static final int ACCOUNT_NAME = 1
        public static final int ACCOUNT_TYPE = 2
        public static final int DISPLAY_NAME = 3
        public static final int PACKAGE_NAME = 4
        public static final int TYPE_RESOURCE_ID = 5
    } 
 
    /**
     * A fake column name that indicates a "Searching..." item in the list. 
     */
 
    private static final String SEARCHING_CURSOR_MARKER = "searching"
 
    /**
     * An asynchronous filter used for loading two data sets: email rows from the local 
     * contact provider and the list of {@link Directory}'s. 
     */
 
    private final class DefaultPartitionFilter extends Filter { 
 
        @Override 
        protected FilterResults performFiltering(CharSequence constraint) { 
            Cursor directoryCursor = null
            if (!mDirectoriesLoaded) { 
                directoryCursor = mContentResolver.query( 
                        DirectoryListQuery.URI, DirectoryListQuery.PROJECTION, nullnullnull); 
                mDirectoriesLoaded = true
            } 
 
            FilterResults results = new FilterResults(); 
            Cursor cursor = null
            if (!TextUtils.isEmpty(constraint)) { 
                Uri.Builder builder = Email.CONTENT_FILTER_URI.buildUpon() 
                        .appendPath(constraint.toString()) 
                        .appendQueryParameter(LIMIT_PARAM_KEY, 
                                String.valueOf(mPreferredMaxResultCount)); 
                if (mAccount != null) { 
                    builder.appendQueryParameter(PRIMARY_ACCOUNT_NAME, mAccount.name); 
                    builder.appendQueryParameter(PRIMARY_ACCOUNT_TYPE, mAccount.type); 
                } 
                Uri uri = builder.build(); 
                cursor = mContentResolver.query(uri, EmailQuery.PROJECTION, nullnullnull); 
                results.count = cursor.getCount(); 
            } 
            results.values = new Cursor[] { directoryCursor, cursor }; 
            return results; 
        } 
 
        @Override 
        protected void publishResults(CharSequence constraint, FilterResults results) { 
            if (results.values != null) { 
                Cursor[] cursors = (Cursor[]) results.values; 
                onDirectoryLoadFinished(constraint, cursors[0], cursors[1]); 
            } 
            results.count = getCount(); 
        } 
 
        @Override 
        public CharSequence convertResultToString(Object resultValue) { 
            return makeDisplayString((Cursor) resultValue); 
        } 
    } 
 
    /**
     * An asynchronous filter that performs search in a particular directory. 
     */
 
    private final class DirectoryPartitionFilter extends Filter { 
        private final int mPartitionIndex; 
        private final long mDirectoryId; 
        private int mLimit; 
 
        public DirectoryPartitionFilter(int partitionIndex, long directoryId) { 
            this.mPartitionIndex = partitionIndex; 
            this.mDirectoryId = directoryId; 
        } 
 
        public synchronized void setLimit(int limit) { 
            this.mLimit = limit; 
        } 
 
        public synchronized int getLimit() { 
            return this.mLimit; 
        } 
 
        @Override 
        protected FilterResults performFiltering(CharSequence constraint) { 
            FilterResults results = new FilterResults(); 
            if (!TextUtils.isEmpty(constraint)) { 
                Uri uri = Email.CONTENT_FILTER_URI.buildUpon() 
                        .appendPath(constraint.toString()) 
                        .appendQueryParameter(DIRECTORY_PARAM_KEY, String.valueOf(mDirectoryId)) 
                        .appendQueryParameter(LIMIT_PARAM_KEY, 
                                String.valueOf(getLimit() + ALLOWANCE_FOR_DUPLICATES)) 
                        .build(); 
                Cursor cursor = mContentResolver.query( 
                        uri, EmailQuery.PROJECTION, nullnullnull); 
                results.values = cursor; 
            } 
            return results; 
        } 
 
        @Override 
        protected void publishResults(CharSequence constraint, FilterResults results) { 
            Cursor cursor = (Cursor) results.values; 
            onPartitionLoadFinished(constraint, mPartitionIndex, cursor); 
            results.count = getCount(); 
        } 
    } 
 
    protected final ContentResolver mContentResolver; 
    private boolean mDirectoriesLoaded; 
    private Account mAccount; 
    private int mPreferredMaxResultCount; 
    private Handler mHandler; 
 
    public BaseEmailAddressAdapter(Context context) { 
        this(context, DEFAULT_PREFERRED_MAX_RESULT_COUNT); 
    } 
 
    public BaseEmailAddressAdapter(Context context, int preferredMaxResultCount) { 
        super(context); 
        mContentResolver = context.getContentResolver(); 
        mPreferredMaxResultCount = preferredMaxResultCount; 
 
        mHandler = new Handler() { 
 
            @Override 
            public void handleMessage(Message msg) { 
                showSearchPendingIfNotComplete(msg.arg1); 
            } 
        }; 
    } 
 
    /**
     * Set the account when known. Causes the search to prioritize contacts from 
     * that account. 
     */
 
    public void setAccount(Account account) { 
        mAccount = account; 
    } 
 
    /**
     * Override to create a view for line item in the autocomplete suggestion list UI. 
     */
 
    protected abstract View inflateItemView(ViewGroup parent); 
 
    /**
     * Override to populate the autocomplete suggestion line item UI with data. 
     */
 
    protected abstract void bindView(View view, String directoryType, String directoryName, 
            String displayName, String emailAddress); 
 
    /**
     * Override to create a view for a "Searching directory" line item, which is 
     * displayed temporarily while the corresponding filter is running. 
     */
 
    protected abstract View inflateItemViewLoading(ViewGroup parent); 
 
    /**
     * Override to populate the "Searching directory" line item UI with data. 
     */
 
    protected abstract void bindViewLoading(View view, String directoryType, String directoryName); 
 
    @Override 
    protected int getItemViewType(int partitionIndex, int position) { 
        DirectoryPartition partition = (DirectoryPartition)getPartition(partitionIndex); 
        return partition.loading ? 1 : 0
    } 
 
    @Override 
    protected View newView(Context context, int partitionIndex, Cursor cursor, 
            int position, ViewGroup parent) { 
        DirectoryPartition partition = (DirectoryPartition)getPartition(partitionIndex); 
        if (partition.loading) { 
            return inflateItemViewLoading(parent); 
        } else { 
            return inflateItemView(parent); 
        } 
    } 
 
    @Override 
    protected void bindView(View v, int partition, Cursor cursor, int position) { 
        DirectoryPartition directoryPartition = (DirectoryPartition)getPartition(partition); 
        String directoryType = directoryPartition.directoryType; 
        String directoryName = directoryPartition.displayName; 
        if (directoryPartition.loading) { 
            bindViewLoading(v, directoryType, directoryName); 
        } else { 
            String displayName = cursor.getString(EmailQuery.NAME); 
            String emailAddress = cursor.getString(EmailQuery.ADDRESS); 
            if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, emailAddress)) { 
                displayName = emailAddress; 
                emailAddress = null
            } 
            bindView(v, directoryType, directoryName, displayName, emailAddress); 
        } 
    } 
 
    @Override 
    public boolean areAllItemsEnabled() { 
        return false
    } 
 
    @Override 
    protected boolean isEnabled(int partitionIndex, int position) { 
        // The "Searching..." item should not be selectable 
        return !isLoading(partitionIndex); 
    } 
 
    private boolean isLoading(int partitionIndex) { 
        return ((DirectoryPartition)getPartition(partitionIndex)).loading; 
    } 
 
    @Override 
    public Filter getFilter() { 
        return new DefaultPartitionFilter(); 
    } 
 
    /**
     * Handles the result of the initial call, which brings back the list of 
     * directories as well as the search results for the local directories. 
     */
 
    protected void onDirectoryLoadFinished
            CharSequence constraint, Cursor directoryCursor, Cursor defaultPartitionCursor) { 
        if (directoryCursor != null) { 
            PackageManager packageManager = getContext().getPackageManager(); 
            DirectoryPartition preferredDirectory = null
            List<DirectoryPartition> directories = new ArrayList<DirectoryPartition>(); 
            while (directoryCursor.moveToNext()) { 
                long id = directoryCursor.getLong(DirectoryListQuery.ID); 
 
                // Skip the local invisible directory, because the default directory 
                // already includes all local results. 
                if (id == DIRECTORY_LOCAL_INVISIBLE) { 
                    continue
                } 
 
                DirectoryPartition partition = new DirectoryPartition(); 
                partition.directoryId = id; 
                partition.displayName = directoryCursor.getString(DirectoryListQuery.DISPLAY_NAME); 
                partition.accountName = directoryCursor.getString(DirectoryListQuery.ACCOUNT_NAME); 
                partition.accountType = directoryCursor.getString(DirectoryListQuery.ACCOUNT_TYPE); 
                String packageName = directoryCursor.getString(DirectoryListQuery.PACKAGE_NAME); 
                int resourceId = directoryCursor.getInt(DirectoryListQuery.TYPE_RESOURCE_ID); 
                if (packageName != null && resourceId != 0) { 
                    try { 
                        Resources resources = 
                                packageManager.getResourcesForApplication(packageName); 
                        partition.directoryType = resources.getString(resourceId); 
                        if (partition.directoryType == null) { 
                            Log.e(TAG, "Cannot resolve directory name: " 
                                    + resourceId + "@" + packageName); 
                        } 
                    } catch (NameNotFoundException e) { 
                        Log.e(TAG, "Cannot resolve directory name: " 
                                + resourceId + "@" + packageName, e); 
                    } 
                } 
 
                // If an account has been provided and we found a directory that 
                // corresponds to that account, place that directory second, directly 
                // underneath the local contacts. 
                if (mAccount != null && mAccount.name.equals(partition.accountName) && 
                        mAccount.type.equals(partition.accountType)) { 
                    preferredDirectory = partition; 
                } else { 
                    directories.add(partition); 
                } 
            } 
 
            if (preferredDirectory != null) { 
                directories.add(1, preferredDirectory); 
            } 
 
            for (DirectoryPartition partition : directories) { 
                addPartition(partition); 
            } 
        } 
 
        int count = getPartitionCount(); 
        int limit = 0
 
        // Since we will be changing several partitions at once, hold the data change 
        // notifications 
        setNotificationsEnabled(false); 
        try { 
            // The filter has loaded results for the default partition too. 
            if (defaultPartitionCursor != null && getPartitionCount() > 0) { 
                changeCursor(0, defaultPartitionCursor); 
            } 
 
            int defaultPartitionCount = (defaultPartitionCursor == null ? 0 
                    : defaultPartitionCursor.getCount()); 
 
            limit = mPreferredMaxResultCount - defaultPartitionCount; 
 
            // Show non-default directories as "loading" 
            // Note: skipping the default partition (index 0), which has already been loaded 
            for (int i = 1; i < count; i++) { 
                DirectoryPartition partition = (DirectoryPartition) getPartition(i); 
                partition.constraint = constraint; 
 
                if (limit > 0) { 
                    if (!partition.loading) { 
                        partition.loading = true
                        changeCursor(i, null); 
                    } 
                } else { 
                    partition.loading = false
                    changeCursor(i, null); 
                } 
            } 
        } finally { 
            setNotificationsEnabled(true); 
        } 
 
        // Start search in other directories 
        // Note: skipping the default partition (index 0), which has already been loaded 
        for (int i = 1; i < count; i++) { 
            DirectoryPartition partition = (DirectoryPartition) getPartition(i); 
            if (partition.loading) { 
                mHandler.removeMessages(MESSAGE_SEARCH_PENDING, partition); 
                Message msg = mHandler.obtainMessage(MESSAGE_SEARCH_PENDING, i, 0, partition); 
                mHandler.sendMessageDelayed(msg, MESSAGE_SEARCH_PENDING_DELAY); 
                if (partition.filter == null) { 
                    partition.filter = new DirectoryPartitionFilter(i, partition.directoryId); 
                } 
                partition.filter.setLimit(limit); 
                partition.filter.filter(constraint); 
            } else { 
                if (partition.filter != null) { 
                    // Cancel any previous loading request 
                    partition.filter.filter(null); 
                } 
            } 
        } 
    } 
 
    void showSearchPendingIfNotComplete(int partitionIndex) { 
        if (partitionIndex < getPartitionCount()) { 
            DirectoryPartition partition = (DirectoryPartition) getPartition(partitionIndex); 
            if (partition.loading) { 
                changeCursor(partitionIndex, createLoadingCursor()); 
            } 
        } 
    } 
 
    /**
     * Creates a dummy cursor to represent the "Searching directory..." item. 
     */
 
    private Cursor createLoadingCursor() { 
        MatrixCursor cursor = new MatrixCursor(new String[]{SEARCHING_CURSOR_MARKER}); 
        cursor.addRow(new Object[]{""}); 
        return cursor; 
    } 
 
    public void onPartitionLoadFinished
            CharSequence constraint, int partitionIndex, Cursor cursor) { 
        if (partitionIndex < getPartitionCount()) { 
            DirectoryPartition partition = (DirectoryPartition) getPartition(partitionIndex); 
 
            // Check if the received result matches the current constraint 
            // If not - the user must have continued typing after the request 
            // was issued 
            if (partition.loading && TextUtils.equals(constraint, partition.constraint)) { 
                partition.loading = false
                mHandler.removeMessages(MESSAGE_SEARCH_PENDING, partition); 
                changeCursor(partitionIndex, removeDuplicatesAndTruncate(partitionIndex, cursor)); 
            } else { 
                // We got the result for an unexpected query (the user is still typing) 
                // Just ignore this result 
                if (cursor != null) { 
                    cursor.close(); 
                } 
            } 
        } else if (cursor != null) { 
            cursor.close(); 
        } 
    } 
 
    /**
     * Post-process the cursor to eliminate duplicates.  Closes the original cursor 
     * and returns a new one. 
     */
 
    private Cursor removeDuplicatesAndTruncate(int partition, Cursor cursor) { 
        if (cursor == null) { 
            return null
        } 
 
        if (cursor.getCount() <= DEFAULT_PREFERRED_MAX_RESULT_COUNT 
                && !hasDuplicates(cursor, partition)) { 
            return cursor; 
        } 
 
        int count = 0
        MatrixCursor newCursor = new MatrixCursor(EmailQuery.PROJECTION); 
        cursor.moveToPosition(-1); 
        while (cursor.moveToNext() && count < DEFAULT_PREFERRED_MAX_RESULT_COUNT) { 
            String displayName = cursor.getString(EmailQuery.NAME); 
            String emailAddress = cursor.getString(EmailQuery.ADDRESS); 
            if (!isDuplicate(emailAddress, partition)) { 
                newCursor.addRow(new Object[]{displayName, emailAddress}); 
                count++; 
            } 
        } 
        cursor.close(); 
 
        return newCursor; 
    } 
 
    private boolean hasDuplicates(Cursor cursor, int partition) { 
        cursor.moveToPosition(-1); 
        while (cursor.moveToNext()) { 
            String emailAddress = cursor.getString(EmailQuery.ADDRESS); 
            if (isDuplicate(emailAddress, partition)) { 
                return true
            } 
        } 
        return false
    } 
 
    /**
     * Checks if the supplied email address is already present in partitions other 
     * than the supplied one. 
     */
 
    private boolean isDuplicate(String emailAddress, int excludePartition) { 
        int partitionCount = getPartitionCount(); 
        for (int partition = 0; partition < partitionCount; partition++) { 
            if (partition != excludePartition && !isLoading(partition)) { 
                Cursor cursor = getCursor(partition); 
                if (cursor != null) { 
                    cursor.moveToPosition(-1); 
                    while (cursor.moveToNext()) { 
                        String address = cursor.getString(EmailQuery.ADDRESS); 
                        if (TextUtils.equals(emailAddress, address)) { 
                            return true
                        } 
                    } 
                } 
            } 
        } 
 
        return false
    } 
 
    private final String makeDisplayString(Cursor cursor) { 
        if (cursor.getColumnName(0).equals(SEARCHING_CURSOR_MARKER)) { 
            return ""
        } 
 
        String displayName = cursor.getString(EmailQuery.NAME); 
        String emailAddress = cursor.getString(EmailQuery.ADDRESS); 
        if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, emailAddress)) { 
             return emailAddress; 
        } else { 
            return new Rfc822Token(displayName, emailAddress, null).toString(); 
        } 
    } 
}