Add Support For Extended Directories

Change-Id: I5097f9d45ce39aad93ede3a01f43e1c58e36c9f1
diff --git a/src/com/android/contacts/common/extensions/ExtendedPhoneDirectoriesManager.java b/src/com/android/contacts/common/extensions/ExtendedPhoneDirectoriesManager.java
new file mode 100644
index 0000000..eb25934
--- /dev/null
+++ b/src/com/android/contacts/common/extensions/ExtendedPhoneDirectoriesManager.java
@@ -0,0 +1,26 @@
+// Copyright 2013 Google Inc. All Rights Reserved.
+
+package com.android.contacts.common.extensions;
+
+import android.content.Context;
+
+import com.android.contacts.common.list.DirectoryPartition;
+
+import java.util.List;
+
+/**
+ * An interface for adding extended phone directories to
+ * {@link com.android.contacts.common.list.PhoneNumberListAdapter}.
+ * An app that wishes to add custom phone directories should implement this class and advertise it
+ * in assets/contacts_extensions.properties. {@link ExtensionsFactory} will load the implementation
+ * and the extended directories will be added by
+ * {@link com.android.contacts.common.list.PhoneNumberListAdapter}.
+ */
+public interface ExtendedPhoneDirectoriesManager {
+
+    /**
+     * Return a list of extended directories to add. May return null if no directories are to be
+     * added.
+     */
+    List<DirectoryPartition> getExtendedDirectories(Context context);
+}
diff --git a/src/com/android/contacts/common/extensions/ExtensionsFactory.java b/src/com/android/contacts/common/extensions/ExtensionsFactory.java
new file mode 100644
index 0000000..d52429e
--- /dev/null
+++ b/src/com/android/contacts/common/extensions/ExtensionsFactory.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2013 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.contacts.common.extensions;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+
+/*
+ * A framework for adding extensions to Dialer. This class reads a property file from
+ * assets/contacts_extensions.properties and loads extension classes that an app has defined. If
+ * an extension class was not defined, null is returned.
+ */
+public class ExtensionsFactory {
+
+    private static String TAG = "ExtensionsFactory";
+
+    // Config filename for mappings of various class names to their custom
+    // implementations.
+    private static final String EXTENSIONS_PROPERTIES = "contacts_extensions.properties";
+
+    private static final String EXTENDED_PHONE_DIRECTORIES_KEY = "extendedPhoneDirectories";
+
+    private static Properties sProperties = null;
+    private static ExtendedPhoneDirectoriesManager mExtendedPhoneDirectoriesManager = null;
+
+    public static void init(Context context) {
+        if (sProperties != null) {
+            return;
+        }
+        try {
+            final InputStream fileStream = context.getAssets().open(EXTENSIONS_PROPERTIES);
+            sProperties = new Properties();
+            sProperties.load(fileStream);
+            fileStream.close();
+
+            final String className = sProperties.getProperty(EXTENDED_PHONE_DIRECTORIES_KEY);
+            if (className != null) {
+                mExtendedPhoneDirectoriesManager = createInstance(className);
+            } else {
+                Log.d(TAG, EXTENDED_PHONE_DIRECTORIES_KEY + " not found in properties file.");
+            }
+
+        } catch (FileNotFoundException e) {
+            // No custom extensions. Ignore.
+            Log.d(TAG, "No custom extensions.");
+        } catch (IOException e) {
+            Log.d(TAG, e.toString());
+        }
+    }
+
+    private static <T> T createInstance(String className) {
+        try {
+            Class<?> c = Class.forName(className);
+            //noinspection unchecked
+            return (T) c.newInstance();
+        } catch (ClassNotFoundException e) {
+            Log.e(TAG, className + ": unable to create instance.", e);
+        } catch (IllegalAccessException e) {
+            Log.e(TAG, className + ": unable to create instance.", e);
+        } catch (InstantiationException e) {
+            Log.e(TAG, className + ": unable to create instance.", e);
+        }
+        return null;
+    }
+
+    public static ExtendedPhoneDirectoriesManager getExtendedPhoneDirectoriesManager() {
+        return mExtendedPhoneDirectoriesManager;
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactEntryListAdapter.java b/src/com/android/contacts/common/list/ContactEntryListAdapter.java
index fb9c73a..c9e74e4 100644
--- a/src/com/android/contacts/common/list/ContactEntryListAdapter.java
+++ b/src/com/android/contacts/common/list/ContactEntryListAdapter.java
@@ -159,7 +159,7 @@
         }
     }
 
-    private int getPartitionByDirectoryId(long id) {
+    protected int getPartitionByDirectoryId(long id) {
         int count = getPartitionCount();
         for (int i = 0; i < count; i++) {
             Partition partition = getPartition(i);
@@ -172,6 +172,20 @@
         return -1;
     }
 
+    protected DirectoryPartition getDirectoryById(long id) {
+        int count = getPartitionCount();
+        for (int i = 0; i < count; i++) {
+            Partition partition = getPartition(i);
+            if (partition instanceof DirectoryPartition) {
+                final DirectoryPartition directoryPartition = (DirectoryPartition) partition;
+                if (directoryPartition.getDirectoryId() == id) {
+                    return directoryPartition;
+                }
+            }
+        }
+        return null;
+    }
+
     public abstract String getContactDisplayName(int position);
     public abstract void configureLoader(CursorLoader loader, long directoryId);
 
@@ -247,6 +261,11 @@
         return mDirectoryResultLimit;
     }
 
+    public int getDirectoryResultLimit(DirectoryPartition directoryPartition) {
+        final int limit = directoryPartition.getResultLimit();
+        return limit == DirectoryPartition.RESULT_LIMIT_DEFAULT ? mDirectoryResultLimit : limit;
+    }
+
     public void setDirectoryResultLimit(int limit) {
         this.mDirectoryResultLimit = limit;
     }
@@ -548,10 +567,10 @@
             countText.setText(R.string.search_results_searching);
         } else {
             int count = cursor == null ? 0 : cursor.getCount();
+            final int limit = getDirectoryResultLimit(directoryPartition);
             if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE
-                    && count >= getDirectoryResultLimit()) {
-                countText.setText(mContext.getString(
-                        R.string.foundTooManyContacts, getDirectoryResultLimit()));
+                    && count >= limit) {
+                countText.setText(mContext.getString(R.string.foundTooManyContacts, limit));
             } else {
                 countText.setText(getQuantityText(
                         count, R.string.listFoundAllContactsZero, R.plurals.searchFoundContacts));
diff --git a/src/com/android/contacts/common/list/DefaultContactListAdapter.java b/src/com/android/contacts/common/list/DefaultContactListAdapter.java
index 6ad9e8b..fb974b4 100644
--- a/src/com/android/contacts/common/list/DefaultContactListAdapter.java
+++ b/src/com/android/contacts/common/list/DefaultContactListAdapter.java
@@ -78,7 +78,7 @@
                         String.valueOf(directoryId));
                 if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE) {
                     builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
-                            String.valueOf(getDirectoryResultLimit()));
+                            String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId))));
                 }
                 builder.appendQueryParameter(SearchSnippetColumns.SNIPPET_ARGS_PARAM_KEY,
                         SNIPPET_ARGS);
diff --git a/src/com/android/contacts/common/list/DirectoryPartition.java b/src/com/android/contacts/common/list/DirectoryPartition.java
index 022d1e6..9a8d4cc 100644
--- a/src/com/android/contacts/common/list/DirectoryPartition.java
+++ b/src/com/android/contacts/common/list/DirectoryPartition.java
@@ -28,12 +28,15 @@
     public static final int STATUS_LOADING = 1;
     public static final int STATUS_LOADED = 2;
 
+    public static final int RESULT_LIMIT_DEFAULT = -1;
+
     private long mDirectoryId;
     private String mDirectoryType;
     private String mDisplayName;
     private int mStatus;
     private boolean mPriorityDirectory;
     private boolean mPhotoSupported;
+    private int mResultLimit = RESULT_LIMIT_DEFAULT;
 
     public DirectoryPartition(boolean showIfEmpty, boolean hasHeader) {
         super(showIfEmpty, hasHeader);
@@ -106,4 +109,17 @@
     public void setPhotoSupported(boolean flag) {
         this.mPhotoSupported = flag;
     }
+
+    /**
+     * Max number of results for this directory. Defaults to {@link #RESULT_LIMIT_DEFAULT} which
+     * implies using the adapter's
+     * {@link com.android.contacts.common.list.ContactListAdapter#getDirectoryResultLimit()}
+     */
+    public int getResultLimit() {
+        return mResultLimit;
+    }
+
+    public void setResultLimit(int resultLimit) {
+        mResultLimit = resultLimit;
+    }
 }
diff --git a/src/com/android/contacts/common/list/PhoneNumberListAdapter.java b/src/com/android/contacts/common/list/PhoneNumberListAdapter.java
index e58e257..43797cc 100644
--- a/src/com/android/contacts/common/list/PhoneNumberListAdapter.java
+++ b/src/com/android/contacts/common/list/PhoneNumberListAdapter.java
@@ -29,13 +29,13 @@
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Directory;
-import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 
 import com.android.contacts.common.R;
-import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.extensions.ExtendedPhoneDirectoriesManager;
+import com.android.contacts.common.extensions.ExtensionsFactory;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -49,8 +49,17 @@
  * API instead of {@link Phone}.
  */
 public class PhoneNumberListAdapter extends ContactEntryListAdapter {
+
     private static final String TAG = PhoneNumberListAdapter.class.getSimpleName();
 
+    // A list of extended directories to add to the directories from the database
+    private final List<DirectoryPartition> mExtendedDirectories;
+
+    // Extended directories will have ID's that are higher than any of the id's from the database.
+    // Thi sis so that we can identify them and set them up properly. If no extended directories
+    // exist, this will be Long.MAX_VALUE
+    private long mFirstExtendedDirectoryId = Long.MAX_VALUE;
+
     public static class PhoneQuery {
         public static final String[] PROJECTION_PRIMARY = new String[] {
             Phone._ID,                          // 0
@@ -97,6 +106,14 @@
         super(context);
         setDefaultFilterHeaderText(R.string.list_filter_phones);
         mUnknownNameText = context.getText(android.R.string.unknownName);
+
+        final ExtendedPhoneDirectoriesManager manager
+                = ExtensionsFactory.getExtendedPhoneDirectoriesManager();
+        if (manager != null) {
+            mExtendedDirectories = manager.getExtendedDirectories(mContext);
+        } else {
+            mExtendedDirectories = null;
+        }
     }
 
     protected CharSequence getUnknownNameText() {
@@ -110,58 +127,77 @@
 
     @Override
     public void configureLoader(CursorLoader loader, long directoryId) {
-        final boolean isRemoteDirectoryQuery = isRemoteDirectory(directoryId);
-        final Builder builder;
-        if (isSearchMode()) {
-            final Uri baseUri;
-            if (isRemoteDirectoryQuery) {
-                baseUri = Phone.CONTENT_FILTER_URI;
-            } else if (mUseCallableUri) {
-                baseUri = Callable.CONTENT_FILTER_URI;
-            } else {
-                baseUri = Phone.CONTENT_FILTER_URI;
-            }
-            builder = baseUri.buildUpon();
-            final String query = getQueryString();
-            if (TextUtils.isEmpty(query)) {
-                builder.appendPath("");
-            } else {
-                builder.appendPath(query);      // Builder will encode the query
-            }
-            builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
-                    String.valueOf(directoryId));
-            if (isRemoteDirectoryQuery) {
-                builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
-                        String.valueOf(getDirectoryResultLimit()));
-            }
-        } else {
-            final Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI;
-            builder = baseUri.buildUpon().appendQueryParameter(
-                    ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT));
-            if (isSectionHeaderDisplayEnabled()) {
-                builder.appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true");
-            }
-            applyFilter(loader, builder, directoryId, getFilter());
+        String query = getQueryString();
+        if (query == null) {
+            query = "";
         }
-
-        // Remove duplicates when it is possible.
-        builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true");
-        loader.setUri(builder.build());
-
-        // TODO a projection that includes the search snippet
-        if (getContactNameDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
+        if (isExtendedDirectory(directoryId)) {
+            final DirectoryPartition directory = getExtendedDirectoryFromId(directoryId);
+            final Builder builder = Uri.parse(directory.getDirectoryType()).buildUpon();
+            builder.appendPath(query);
+            builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
+                    String.valueOf(getDirectoryResultLimit(directory)));
+            loader.setUri(builder.build());
             loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
         } else {
-            loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE);
-        }
+            final boolean isRemoteDirectoryQuery = isRemoteDirectory(directoryId);
+            final Builder builder;
+            if (isSearchMode()) {
+                final Uri baseUri;
+                if (isRemoteDirectoryQuery) {
+                    baseUri = Phone.CONTENT_FILTER_URI;
+                } else if (mUseCallableUri) {
+                    baseUri = Callable.CONTENT_FILTER_URI;
+                } else {
+                    baseUri = Phone.CONTENT_FILTER_URI;
+                }
+                builder = baseUri.buildUpon();
+                builder.appendPath(query);      // Builder will encode the query
+                builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+                        String.valueOf(directoryId));
+                if (isRemoteDirectoryQuery) {
+                    builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
+                            String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId))));
+                }
+            } else {
+                final Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI;
+                builder = baseUri.buildUpon().appendQueryParameter(
+                        ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT));
+                if (isSectionHeaderDisplayEnabled()) {
+                    builder.appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true");
+                }
+                applyFilter(loader, builder, directoryId, getFilter());
+            }
 
-        if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
-            loader.setSortOrder(Phone.SORT_KEY_PRIMARY);
-        } else {
-            loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE);
+            // Remove duplicates when it is possible.
+            builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true");
+            loader.setUri(builder.build());
+
+            // TODO a projection that includes the search snippet
+            if (getContactNameDisplayOrder() ==
+                    ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
+                loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
+            } else {
+                loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE);
+            }
+
+            if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
+                loader.setSortOrder(Phone.SORT_KEY_PRIMARY);
+            } else {
+                loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE);
+            }
         }
     }
 
+    private boolean isExtendedDirectory(long directoryId) {
+        return directoryId >= mFirstExtendedDirectoryId;
+    }
+
+    private DirectoryPartition getExtendedDirectoryFromId(long directoryId) {
+        final int directoryIndex = (int) (directoryId - mFirstExtendedDirectoryId);
+        return mExtendedDirectories.get(directoryIndex);
+    }
+
     /**
      * Configure {@code loader} and {@code uriBuilder} according to {@code directoryId} and {@code
      * filter}.
@@ -368,4 +404,59 @@
     public boolean usesCallableUri() {
         return mUseCallableUri;
     }
+
+    /**
+     * Override base implementation to inject extended directories between local & remote
+     * directories. This is done in the following steps:
+     * 1. Call base implementation to add directories from the cursor.
+     * 2. Iterate all base directories and establish the following information:
+     *   a. The highest directory id so that we can assign unused id's to the extended directories.
+     *   b. The index of the last non-remote directory. This is where we will insert extended
+     *      directories.
+     * 3. Iterate the extended directories and for each one, assign an ID and insert it in the
+     *    proper location.
+     */
+    @Override
+    public void changeDirectories(Cursor cursor) {
+        super.changeDirectories(cursor);
+        if (getDirectorySearchMode() == DirectoryListLoader.SEARCH_MODE_NONE) {
+            return;
+        }
+        final int numExtendedDirectories = mExtendedDirectories.size();
+        if (getPartitionCount() == cursor.getCount() + numExtendedDirectories) {
+            // already added all directories;
+            return;
+        }
+        //
+        mFirstExtendedDirectoryId = Long.MAX_VALUE;
+        if (numExtendedDirectories > 0) {
+            // The Directory.LOCAL_INVISIBLE is not in the cursor but we can't reuse it's
+            // "special" ID.
+            long maxId = Directory.LOCAL_INVISIBLE;
+            int insertIndex = 0;
+            for (int i = 0, n = getPartitionCount(); i < n; i++) {
+                final DirectoryPartition partition = (DirectoryPartition) getPartition(i);
+                final long id = partition.getDirectoryId();
+                if (id > maxId) {
+                    maxId = id;
+                }
+                if (!isRemoteDirectory(id)) {
+                    // assuming remote directories come after local, we will end up with the index
+                    // where we should insert extended directories. This also works if there are no
+                    // remote directories at all.
+                    insertIndex = i + 1;
+                }
+            }
+            // Extended directories ID's cannot collide with base directories
+            mFirstExtendedDirectoryId = maxId + 1;
+            for (int i = 0; i < numExtendedDirectories; i++) {
+                final long id = mFirstExtendedDirectoryId + i;
+                final DirectoryPartition directory = mExtendedDirectories.get(i);
+                if (getPartitionByDirectoryId(id) == -1) {
+                    addPartition(insertIndex, directory);
+                    directory.setDirectoryId(id);
+                }
+            }
+        }
+    }
 }