Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fddbb83d authored by Walter Jang's avatar Walter Jang
Browse files

First pass on yenta autocomplete powered search (1/2)

Test: Basic searches with the autocomplete experiment on/off
Bug: 30436991

Change-Id: I9b756bd65a9714542c879cbd2a056d34a51cc359
parent 13a70d48
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ package com.android.contactsbind;

import com.android.contacts.common.logging.Logger;
import com.android.contacts.common.preference.PreferenceManager;
import com.android.contactsbind.search.AutocompleteHelper;

import android.content.Context;

@@ -28,4 +29,8 @@ public class ObjectFactory {
    }

    public static PreferenceManager getPreferenceManager(Context context) { return null; }

    public static AutocompleteHelper getAutocompleteHelper(Context context) {
        return null;
    }
}
+39 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.contactsbind.search;

import android.database.Cursor;

public final class AutocompleteHelper {

    public static final String TAG = "Autocomplete";

    public interface Listener {
        void onAutocompletesAvailable(Cursor cursor);
    }

    private AutocompleteHelper() {
    }

    public void setListener(Listener listener) {
    }

    public void setProjection(String[] projection) {
    }

    public void setQuery(String query) {
    }
}
 No newline at end of file
+8 −0
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.provider.ContactsContract.SearchSnippets;
import android.text.TextUtils;
import android.view.View;

import com.android.contacts.common.Experiments;
import com.android.contacts.common.compat.ContactsCompat;
import com.android.contacts.common.model.account.AccountWithDataSet;
import com.android.contacts.common.preference.ContactsPreferences;
@@ -115,6 +116,13 @@ public class DefaultContactListAdapter extends ContactListAdapter {
                loader.setUri(builder.build());
                loader.setProjection(getProjection(true));
                sortOrder = STREQUENT_SORT;
                if (Flags.getInstance(getContext()).getBoolean(Experiments.SEARCH_YENTA)
                        && loader instanceof FavoritesAndContactsLoader
                        && directoryId == Directory.DEFAULT) {
                    final FavoritesAndContactsLoader favoritesAndContactsLoader =
                            (FavoritesAndContactsLoader) loader;
                    favoritesAndContactsLoader.setAutocompleteQuery(query);
                }
            }
        } else {
            final ContactListFilter filter = getFilter();
+68 −45
Original line number Diff line number Diff line
@@ -19,29 +19,33 @@ import android.content.Context;
import android.content.CursorLoader;
import android.database.Cursor;
import android.database.MergeCursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract.Contacts;
import android.util.Log;

import com.android.contactsbind.ObjectFactory;
import com.android.contactsbind.search.AutocompleteHelper;
import com.google.common.collect.Lists;

import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * A loader for use in the default contact list, which will also query for favorite contacts
 * if configured to do so.
 */
public class FavoritesAndContactsLoader extends CursorLoader {
public class FavoritesAndContactsLoader extends CursorLoader implements AutocompleteHelper.Listener {

    private static final int AUTOCOMPLETE_TIMEOUT_MS = 1000;

    private boolean mLoadFavorites;

    private String[] mProjection;

    private Uri mExtraUri;
    private String[] mExtraProjection;
    private String mExtraSelection;
    private String[] mExtraSelectionArgs;
    private boolean mMergeExtraContactsAfterPrimary;
    private String mAutocompleteQuery;
    private CountDownLatch mAutocompleteLatch = new CountDownLatch(1);
    private Cursor mAutocompleteCursor;

    public FavoritesAndContactsLoader(Context context) {
        super(context);
@@ -52,54 +56,51 @@ public class FavoritesAndContactsLoader extends CursorLoader {
        mLoadFavorites = flag;
    }

    public void setAutocompleteQuery(String autocompleteQuery) {
        mAutocompleteQuery = autocompleteQuery;
    }

    public void setProjection(String[] projection) {
        super.setProjection(projection);
        mProjection = projection;
    }

    /** Configure an extra query and merge results in before the primary results. */
    public void setLoadExtraContactsFirst(Uri uri, String[] projection) {
        mExtraUri = uri;
        mExtraProjection = projection;
        mMergeExtraContactsAfterPrimary = false;
    }

    /** Configure an extra query and merge results in after the primary results. */
    public void setLoadExtraContactsLast(Uri uri, String[] projection, String selection,
            String[] selectionArgs) {
        mExtraUri = uri;
        mExtraProjection = projection;
        mExtraSelection = selection;
        mExtraSelectionArgs = selectionArgs;
        mMergeExtraContactsAfterPrimary = true;
    }

    private boolean canLoadExtraContacts() {
        return mExtraUri != null && mExtraProjection != null;
    }

    @Override
    public Cursor loadInBackground() {
        List<Cursor> cursors = Lists.newArrayList();
        if (mLoadFavorites) {
            cursors.add(loadFavoritesContacts());
        }
        if (canLoadExtraContacts() && !mMergeExtraContactsAfterPrimary) {
            cursors.add(loadExtraContacts());
        }
        // ContactsCursor.loadInBackground() can return null; MergeCursor
        // correctly handles null cursors.
        Cursor cursor = null;

        if (mAutocompleteQuery != null) {
            final AutocompleteHelper autocompleteHelper =
                    ObjectFactory.getAutocompleteHelper(getContext());
            if (autocompleteHelper != null) {
                autocompleteHelper.setListener(this);
                autocompleteHelper.setProjection(mProjection);
                autocompleteHelper.setQuery(mAutocompleteQuery);
                try {
            cursor = super.loadInBackground();
        } catch (NullPointerException | SecurityException e) {
            // Ignore NPEs and SecurityExceptions thrown by providers
                    if (!mAutocompleteLatch.await(AUTOCOMPLETE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
                        logw("Timeout expired before receiving autocompletions");
                    }
                } catch (InterruptedException e) {
                    logw("Interrupted while waiting for autocompletions");
                }
                if (mAutocompleteCursor != null) {
                    cursors.add(mAutocompleteCursor);
                    // TODO: exclude these results from the main loader results, see b/30742359
                }
            }
        final Cursor contactsCursor = cursor;
        }

        // TODO: if the autocomplete experiment in on, only show those results even if they're empty
        final Cursor contactsCursor = mAutocompleteQuery == null ? loadContacts() : null;
        if (mAutocompleteQuery == null) {
            cursors.add(contactsCursor);
        if (canLoadExtraContacts() && mMergeExtraContactsAfterPrimary) {
            cursors.add(loadExtraContacts());
        }
        // Guard against passing an empty array to the MergeCursor constructor
        if (cursors.isEmpty()) cursors.add(null);

        return new MergeCursor(cursors.toArray(new Cursor[cursors.size()])) {
            @Override
            public Bundle getExtras() {
@@ -109,9 +110,15 @@ public class FavoritesAndContactsLoader extends CursorLoader {
        };
    }

    private Cursor loadExtraContacts() {
        return getContext().getContentResolver().query(
                mExtraUri, mExtraProjection, mExtraSelection, mExtraSelectionArgs, null);
    private Cursor loadContacts() {
        // ContactsCursor.loadInBackground() can return null; MergeCursor
        // correctly handles null cursors.
        try {
            return super.loadInBackground();
        } catch (NullPointerException | SecurityException e) {
            // Ignore NPEs and SecurityExceptions thrown by providers
        }
        return null;
    }

    private Cursor loadFavoritesContacts() {
@@ -119,4 +126,20 @@ public class FavoritesAndContactsLoader extends CursorLoader {
                Contacts.CONTENT_URI, mProjection, Contacts.STARRED + "=?", new String[]{"1"},
                getSortOrder());
    }

    @Override
    public void onAutocompletesAvailable(Cursor cursor) {
        if (cursor == null || cursor.getCount() == 0) {
            logw("Ignoring null or empty autocompletions");
        } else {
            mAutocompleteCursor = cursor;
            mAutocompleteLatch.countDown();
        }
    }

    private static void logw(String message) {
        if (Log.isLoggable(AutocompleteHelper.TAG, Log.WARN)) {
            Log.w(AutocompleteHelper.TAG, message);
        }
    }
}