summaryrefslogtreecommitdiffstats
path: root/java/com/android/dialer/lookup/LookupProvider.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/dialer/lookup/LookupProvider.java')
-rw-r--r--java/com/android/dialer/lookup/LookupProvider.java504
1 files changed, 504 insertions, 0 deletions
diff --git a/java/com/android/dialer/lookup/LookupProvider.java b/java/com/android/dialer/lookup/LookupProvider.java
new file mode 100644
index 000000000..b62a94af1
--- /dev/null
+++ b/java/com/android/dialer/lookup/LookupProvider.java
@@ -0,0 +1,504 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.lookup;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.location.Criteria;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Contacts;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.contacts.common.list.DirectoryPartition;
+import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.util.PermissionsUtil;
+import com.android.dialer.R;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.TimeUnit;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class LookupProvider extends ContentProvider {
+ private static final String TAG = LookupProvider.class.getSimpleName();
+
+ private static final boolean DEBUG = false;
+
+ public static List<DirectoryPartition> getExtendedDirectories(Context context) {
+ ArrayList<DirectoryPartition> list = new ArrayList<DirectoryPartition>();
+
+ // The directories are shown in reverse order, so insert forward lookup
+ // last to make it show up at the top
+
+ if (LookupSettings.isPeopleLookupEnabled(context)) {
+ DirectoryPartition dp = new DirectoryPartition(false, true);
+ dp.setContentUri(PEOPLE_LOOKUP_URI.toString());
+ dp.setLabel(context.getString(R.string.people));
+ dp.setPriorityDirectory(false);
+ dp.setPhotoSupported(true);
+ dp.setDisplayNumber(false);
+ dp.setResultLimit(3);
+ list.add(dp);
+ } else {
+ Log.i(TAG, "Forward lookup (people) is disabled");
+ }
+
+ if (LookupSettings.isForwardLookupEnabled(context)) {
+ DirectoryPartition dp = new DirectoryPartition(false, true);
+ dp.setContentUri(NEARBY_LOOKUP_URI.toString());
+ dp.setLabel(context.getString(R.string.nearby_places));
+ dp.setPriorityDirectory(false);
+ dp.setPhotoSupported(true);
+ dp.setDisplayNumber(false);
+ dp.setResultLimit(3);
+ list.add(dp);
+ } else {
+ Log.i(TAG, "Forward lookup (nearby places) is disabled");
+ }
+
+ return list;
+ }
+
+ public static final String AUTHORITY = "com.android.dialer.lookup";
+ public static final Uri AUTHORITY_URI =
+ Uri.parse("content://" + AUTHORITY);
+ public static final Uri NEARBY_LOOKUP_URI =
+ Uri.withAppendedPath(AUTHORITY_URI, "nearby");
+ public static final Uri PEOPLE_LOOKUP_URI =
+ Uri.withAppendedPath(AUTHORITY_URI, "people");
+ public static final Uri IMAGE_CACHE_URI =
+ Uri.withAppendedPath(AUTHORITY_URI, "images");
+
+ private static final UriMatcher sURIMatcher = new UriMatcher(-1);
+ private final LinkedList<FutureTask> mActiveTasks =
+ new LinkedList<FutureTask>();
+
+ private static final int NEARBY = 0;
+ private static final int PEOPLE = 1;
+ private static final int IMAGE = 2;
+
+ static {
+ sURIMatcher.addURI(AUTHORITY, "nearby/*", NEARBY);
+ sURIMatcher.addURI(AUTHORITY, "people/*", PEOPLE);
+ sURIMatcher.addURI(AUTHORITY, "images/*", IMAGE);
+ }
+
+ private class FutureCallable<T> implements Callable<T> {
+ private final Callable<T> mCallable;
+ private volatile FutureTask<T> mFuture;
+
+ public FutureCallable(Callable<T> callable) {
+ mFuture = null;
+ mCallable = callable;
+ }
+
+ public T call() throws Exception {
+ Log.v(TAG, "Future called for " + Thread.currentThread().getName());
+
+ T result = mCallable.call();
+ if (mFuture == null) {
+ return result;
+ }
+
+ synchronized (mActiveTasks) {
+ mActiveTasks.remove(mFuture);
+ }
+
+ mFuture = null;
+ return result;
+ }
+
+ public void setFuture(FutureTask<T> future) {
+ mFuture = future;
+ }
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, final String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ if (DEBUG) Log.v(TAG, "query: " + uri);
+
+ Location lastLocation = null;
+ final int match = sURIMatcher.match(uri);
+
+ switch (match) {
+ case NEARBY:
+ if (!PermissionsUtil.hasLocationPermissions(getContext())) {
+ Log.v(TAG, "Location permission is missing, ignoring query.");
+ return null;
+ }
+ if (!isLocationEnabled()) {
+ Log.v(TAG, "Location settings is disabled, ignoring query.");
+ return null;
+ }
+ lastLocation = getLastLocation();
+ if (lastLocation == null) {
+ Log.v(TAG, "No location available, ignoring query.");
+ return null;
+ }
+ // fall through to the actual query
+
+ case PEOPLE:
+ final String filter = Uri.encode(uri.getLastPathSegment());
+ String limit = uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY);
+
+ int maxResults = -1;
+
+ try {
+ if (limit != null) {
+ maxResults = Integer.parseInt(limit);
+ }
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "query: invalid limit parameter: '" + limit + "'");
+ }
+
+ final Location finalLastLocation = lastLocation;
+ final int finalMaxResults = maxResults;
+
+ return execute(new Callable<Cursor>() {
+ @Override
+ public Cursor call() {
+ return handleFilter(match, projection, filter,
+ finalMaxResults, finalLastLocation);
+ }
+ }, "FilterThread");
+ }
+
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ throw new UnsupportedOperationException("insert() not supported");
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+ throw new UnsupportedOperationException("update() not supported");
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException("delete() not supported");
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ int match = sURIMatcher.match(uri);
+
+ switch (match) {
+ case NEARBY:
+ case PEOPLE:
+ return Contacts.CONTENT_ITEM_TYPE;
+
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode)
+ throws FileNotFoundException {
+ switch (sURIMatcher.match(uri)) {
+ case IMAGE:
+ String number = uri.getLastPathSegment();
+
+ File image = LookupCache.getImagePath(getContext(), number);
+
+ if (mode.equals("r")) {
+ if (image == null || !image.exists() || !image.isFile()) {
+ throw new FileNotFoundException("Cached image does not exist");
+ }
+
+ return ParcelFileDescriptor.open(image,
+ ParcelFileDescriptor.MODE_READ_ONLY);
+ } else {
+ throw new FileNotFoundException("The URI is read only");
+ }
+
+ default:
+ throw new FileNotFoundException("Invalid URI: " + uri);
+ }
+ }
+
+ /**
+ * Check if the location services is on.
+ *
+ * @return Whether location services are enabled
+ */
+ private boolean isLocationEnabled() {
+ try {
+ int mode = Settings.Secure.getInt(
+ getContext().getContentResolver(),
+ Settings.Secure.LOCATION_MODE);
+
+ return mode != Settings.Secure.LOCATION_MODE_OFF;
+ } catch (Settings.SettingNotFoundException e) {
+ Log.e(TAG, "Failed to get location mode", e);
+ return false;
+ }
+ }
+
+ /**
+ * Get location from last location query.
+ *
+ * @return The last location
+ */
+ private Location getLastLocation() {
+ LocationManager locationManager = (LocationManager)
+ getContext().getSystemService(Context.LOCATION_SERVICE);
+
+ try {
+ locationManager.requestSingleUpdate(new Criteria(),
+ new LocationListener() {
+ @Override
+ public void onLocationChanged(Location location) {
+ }
+
+ @Override
+ public void onProviderDisabled(String provider) {
+ }
+
+ @Override
+ public void onProviderEnabled(String provider) {
+ }
+
+ @Override
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ }
+ }, Looper.getMainLooper());
+
+ return locationManager.getLastLocation();
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Process filter/query and perform the lookup.
+ *
+ * @param projection Columns to include in query
+ * @param filter String to lookup
+ * @param maxResults Maximum number of results
+ * @param lastLocation Coordinates of last location query
+ * @return Cursor for the results
+ */
+ private Cursor handleFilter(int type, String[] projection, String filter,
+ int maxResults, Location lastLocation) {
+ if (DEBUG) Log.v(TAG, "handleFilter(" + filter + ")");
+
+ if (filter != null) {
+ try {
+ filter = URLDecoder.decode(filter, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ }
+
+ ContactInfo[] results = null;
+ if (type == NEARBY) {
+ ForwardLookup fl = ForwardLookup.getInstance(getContext());
+ results = fl.lookup(getContext(), filter, lastLocation);
+ } else if (type == PEOPLE) {
+ PeopleLookup pl = PeopleLookup.getInstance(getContext());
+ results = pl.lookup(getContext(), filter);
+ }
+
+ if (results == null || results.length == 0) {
+ if (DEBUG) Log.v(TAG, "handleFilter(" + filter + "): No results");
+ return null;
+ }
+
+ Cursor cur = null;
+ try {
+ cur = buildResultCursor(projection, results, maxResults);
+
+ if (DEBUG) Log.v(TAG, "handleFilter(" + filter + "): "
+ + cur.getCount() + " matches");
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON failure", e);
+ }
+
+ return cur;
+ }
+
+ return null;
+ }
+
+ /**
+ * Query results.
+ *
+ * @param projection Columns to include in query
+ * @param results Results for the forward lookup
+ * @param maxResults Maximum number of rows/results to add to cursor
+ * @return Cursor for forward lookup query results
+ */
+ private Cursor buildResultCursor(String[] projection,
+ ContactInfo[] results, int maxResults)
+ throws JSONException {
+ // Extended directories always use this projection
+ MatrixCursor cursor = new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY);
+
+ int id = 1;
+
+ for (int i = 0; i < results.length; i++) {
+ Object[] row = new Object[PhoneQuery.PROJECTION_PRIMARY.length];
+
+ row[PhoneQuery.PHONE_ID] = id;
+ row[PhoneQuery.PHONE_TYPE] = results[i].type;
+ row[PhoneQuery.PHONE_LABEL] = getAddress(results[i]);
+ row[PhoneQuery.PHONE_NUMBER] = results[i].number;
+ row[PhoneQuery.CONTACT_ID] = id;
+ row[PhoneQuery.LOOKUP_KEY] = results[i].lookupUri.getEncodedFragment();
+ row[PhoneQuery.PHOTO_ID] = 0;
+ row[PhoneQuery.DISPLAY_NAME] = results[i].name;
+ row[PhoneQuery.PHOTO_URI] = results[i].photoUri;
+
+ cursor.addRow(row);
+
+ if (maxResults != -1 && cursor.getCount() >= maxResults) {
+ break;
+ }
+
+ id++;
+ }
+
+ return cursor;
+ }
+
+ private String getAddress(ContactInfo info) {
+ // Hack: Show city or address for phone label, so they appear in
+ // the results list
+
+ String city = null;
+ String address = null;
+
+ try {
+ String jsonString = info.lookupUri.getEncodedFragment();
+ JSONObject json = new JSONObject(jsonString);
+ JSONObject contact = json.getJSONObject(Contacts.CONTENT_ITEM_TYPE);
+
+ if (!contact.has(StructuredPostal.CONTENT_ITEM_TYPE)) {
+ return null;
+ }
+
+ JSONArray addresses = contact.getJSONArray(
+ StructuredPostal.CONTENT_ITEM_TYPE);
+
+ if (addresses.length() == 0) {
+ return null;
+ }
+
+ JSONObject addressEntry = addresses.getJSONObject(0);
+
+ if (addressEntry.has(StructuredPostal.CITY)) {
+ city = addressEntry.getString(StructuredPostal.CITY);
+ }
+ if (addressEntry.has(StructuredPostal.FORMATTED_ADDRESS)) {
+ address = addressEntry.getString(
+ StructuredPostal.FORMATTED_ADDRESS);
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Failed to get address", e);
+ }
+
+ if (city != null) {
+ return city;
+ } else if (address != null) {
+ return address;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Execute thread that is killed after a specified amount of time.
+ *
+ * @param callable The thread
+ * @param name Name of the thread
+ * @return Instance of the thread
+ */
+ private <T> T execute(Callable<T> callable, String name) {
+ FutureCallable<T> futureCallable = new FutureCallable<T>(callable);
+ FutureTask<T> future = new FutureTask<T>(futureCallable);
+ futureCallable.setFuture(future);
+
+ synchronized (mActiveTasks) {
+ mActiveTasks.addLast(future);
+ Log.v(TAG, "Currently running tasks: " + mActiveTasks.size());
+
+ while (mActiveTasks.size() > 8) {
+ Log.w(TAG, "Too many tasks, canceling one");
+ mActiveTasks.removeFirst().cancel(true);
+ }
+ }
+
+ Log.v(TAG, "Starting task " + name);
+
+ new Thread(future, name).start();
+
+ try {
+ Log.v(TAG, "Getting future " + name);
+ return future.get(10000, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Task was interrupted: " + name);
+ Thread.currentThread().interrupt();
+ } catch (ExecutionException e) {
+ Log.w(TAG, "Task threw an exception: " + name, e);
+ } catch (TimeoutException e) {
+ Log.w(TAG, "Task timed out: " + name);
+ future.cancel(true);
+ } catch (CancellationException e) {
+ Log.w(TAG, "Task was cancelled: " + name);
+ }
+
+ return null;
+ }
+}