| /* |
| * 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.dialer.smartdial; |
| |
| import android.content.AsyncTaskLoader; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.database.MatrixCursor; |
| import android.provider.ContactsContract.CommonDataKinds.Phone; |
| import com.android.dialer.common.LogUtil; |
| import com.android.dialer.database.Database; |
| import com.android.dialer.database.DialerDatabaseHelper; |
| import com.android.dialer.database.DialerDatabaseHelper.ContactNumber; |
| import com.android.dialer.smartdial.util.SmartDialNameMatcher; |
| import com.android.dialer.util.PermissionsUtil; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** Implements a Loader<Cursor> class to asynchronously load SmartDial search results. */ |
| public class SmartDialCursorLoader extends AsyncTaskLoader<Cursor> { |
| |
| private static final String TAG = "SmartDialCursorLoader"; |
| private static final boolean DEBUG = false; |
| |
| private final Context context; |
| |
| private Cursor cursor; |
| |
| private String query; |
| private SmartDialNameMatcher nameMatcher; |
| |
| private boolean showEmptyListForNullQuery = true; |
| |
| public SmartDialCursorLoader(Context context) { |
| super(context); |
| this.context = context; |
| } |
| |
| /** |
| * Configures the query string to be used to find SmartDial matches. |
| * |
| * @param query The query string user typed. |
| */ |
| public void configureQuery(String query) { |
| if (DEBUG) { |
| LogUtil.v(TAG, "Configure new query to be " + query); |
| } |
| this.query = SmartDialNameMatcher.normalizeNumber(context, query); |
| |
| /** Constructs a name matcher object for matching names. */ |
| nameMatcher = new SmartDialNameMatcher(this.query); |
| nameMatcher.setShouldMatchEmptyQuery(!showEmptyListForNullQuery); |
| } |
| |
| /** |
| * Queries the SmartDial database and loads results in background. |
| * |
| * @return Cursor of contacts that matches the SmartDial query. |
| */ |
| @Override |
| public Cursor loadInBackground() { |
| if (DEBUG) { |
| LogUtil.v(TAG, "Load in background " + query); |
| } |
| |
| if (!PermissionsUtil.hasContactsReadPermissions(context)) { |
| return new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); |
| } |
| |
| /** Loads results from the database helper. */ |
| final DialerDatabaseHelper dialerDatabaseHelper = |
| Database.get(context).getDatabaseHelper(context); |
| final ArrayList<ContactNumber> allMatches = |
| dialerDatabaseHelper.getLooseMatches(query, nameMatcher); |
| |
| if (DEBUG) { |
| LogUtil.v(TAG, "Loaded matches " + allMatches.size()); |
| } |
| |
| /** Constructs a cursor for the returned array of results. */ |
| final MatrixCursor cursor = new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); |
| Object[] row = new Object[PhoneQuery.PROJECTION_PRIMARY.length]; |
| for (ContactNumber contact : allMatches) { |
| row[PhoneQuery.PHONE_ID] = contact.dataId; |
| row[PhoneQuery.PHONE_NUMBER] = contact.phoneNumber; |
| row[PhoneQuery.CONTACT_ID] = contact.id; |
| row[PhoneQuery.LOOKUP_KEY] = contact.lookupKey; |
| row[PhoneQuery.PHOTO_ID] = contact.photoId; |
| row[PhoneQuery.DISPLAY_NAME] = contact.displayName; |
| row[PhoneQuery.CARRIER_PRESENCE] = contact.carrierPresence; |
| cursor.addRow(row); |
| } |
| return cursor; |
| } |
| |
| @Override |
| public void deliverResult(Cursor cursor) { |
| if (isReset()) { |
| /** The Loader has been reset; ignore the result and invalidate the data. */ |
| releaseResources(cursor); |
| return; |
| } |
| |
| /** Hold a reference to the old data so it doesn't get garbage collected. */ |
| Cursor oldCursor = this.cursor; |
| this.cursor = cursor; |
| |
| if (isStarted()) { |
| /** If the Loader is in a started state, deliver the results to the client. */ |
| super.deliverResult(cursor); |
| } |
| |
| /** Invalidate the old data as we don't need it any more. */ |
| if (oldCursor != null && oldCursor != cursor) { |
| releaseResources(oldCursor); |
| } |
| } |
| |
| @Override |
| protected void onStartLoading() { |
| if (cursor != null) { |
| /** Deliver any previously loaded data immediately. */ |
| deliverResult(cursor); |
| } |
| if (cursor == null) { |
| /** Force loads every time as our results change with queries. */ |
| forceLoad(); |
| } |
| } |
| |
| @Override |
| protected void onStopLoading() { |
| /** The Loader is in a stopped state, so we should attempt to cancel the current load. */ |
| cancelLoad(); |
| } |
| |
| @Override |
| protected void onReset() { |
| /** Ensure the loader has been stopped. */ |
| onStopLoading(); |
| |
| /** Release all previously saved query results. */ |
| if (cursor != null) { |
| releaseResources(cursor); |
| cursor = null; |
| } |
| } |
| |
| @Override |
| public void onCanceled(Cursor cursor) { |
| super.onCanceled(cursor); |
| |
| /** The load has been canceled, so we should release the resources associated with 'data'. */ |
| releaseResources(cursor); |
| } |
| |
| private void releaseResources(Cursor cursor) { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| |
| public void setShowEmptyListForNullQuery(boolean show) { |
| showEmptyListForNullQuery = show; |
| if (nameMatcher != null) { |
| nameMatcher.setShouldMatchEmptyQuery(!show); |
| } |
| } |
| |
| /** Moved from contacts/common, contains all of the projections needed for Smart Dial queries. */ |
| public static class PhoneQuery { |
| |
| public static final String[] PROJECTION_PRIMARY_INTERNAL = |
| new String[] { |
| Phone._ID, // 0 |
| Phone.TYPE, // 1 |
| Phone.LABEL, // 2 |
| Phone.NUMBER, // 3 |
| Phone.CONTACT_ID, // 4 |
| Phone.LOOKUP_KEY, // 5 |
| Phone.PHOTO_ID, // 6 |
| Phone.DISPLAY_NAME_PRIMARY, // 7 |
| Phone.PHOTO_THUMBNAIL_URI, // 8 |
| }; |
| |
| public static final String[] PROJECTION_PRIMARY; |
| public static final String[] PROJECTION_ALTERNATIVE_INTERNAL = |
| new String[] { |
| Phone._ID, // 0 |
| Phone.TYPE, // 1 |
| Phone.LABEL, // 2 |
| Phone.NUMBER, // 3 |
| Phone.CONTACT_ID, // 4 |
| Phone.LOOKUP_KEY, // 5 |
| Phone.PHOTO_ID, // 6 |
| Phone.DISPLAY_NAME_ALTERNATIVE, // 7 |
| Phone.PHOTO_THUMBNAIL_URI, // 8 |
| }; |
| public static final String[] PROJECTION_ALTERNATIVE; |
| public static final int PHONE_ID = 0; |
| public static final int PHONE_TYPE = 1; |
| public static final int PHONE_LABEL = 2; |
| public static final int PHONE_NUMBER = 3; |
| public static final int CONTACT_ID = 4; |
| public static final int LOOKUP_KEY = 5; |
| public static final int PHOTO_ID = 6; |
| public static final int DISPLAY_NAME = 7; |
| public static final int PHOTO_URI = 8; |
| public static final int CARRIER_PRESENCE = 9; |
| |
| static { |
| final List<String> projectionList = |
| new ArrayList<>(Arrays.asList(PROJECTION_PRIMARY_INTERNAL)); |
| projectionList.add(Phone.CARRIER_PRESENCE); // 9 |
| PROJECTION_PRIMARY = projectionList.toArray(new String[projectionList.size()]); |
| } |
| |
| static { |
| final List<String> projectionList = |
| new ArrayList<>(Arrays.asList(PROJECTION_ALTERNATIVE_INTERNAL)); |
| projectionList.add(Phone.CARRIER_PRESENCE); // 9 |
| PROJECTION_ALTERNATIVE = projectionList.toArray(new String[projectionList.size()]); |
| } |
| } |
| } |