summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRohit Yengisetty <rohit@cyngn.com>2015-10-08 18:31:42 -0700
committerRohit Yengisetty <rohit@cyngn.com>2015-12-14 14:29:55 -0800
commit4852f70aaa45078b6874c146d38bf872f6b6b509 (patch)
tree726178fd8411206ace7ce36b8e8c2c362afd87d5
parent7394f186d54d676516bd2766be39ab8182d1614e (diff)
downloadpackages_providers_ContactsProvider-4852f70aaa45078b6874c146d38bf872f6b6b509.zip
packages_providers_ContactsProvider-4852f70aaa45078b6874c146d38bf872f6b6b509.tar.gz
packages_providers_ContactsProvider-4852f70aaa45078b6874c146d38bf872f6b6b509.tar.bz2
Add a richer system around preloading contacts
Change-Id: I2c3b6b79ee41eb73948f9a053454654ee8566a12
-rw-r--r--res/raw/preloaded_contacts.json0
-rw-r--r--res/raw/preloaded_contacts_schema.json59
-rw-r--r--res/values/config.xml3
-rw-r--r--src/com/android/providers/contacts/Constants.java3
-rw-r--r--src/com/android/providers/contacts/ContactsProvider2.java52
-rw-r--r--src/com/android/providers/contacts/util/PreloadedContactsFileParser.java225
6 files changed, 342 insertions, 0 deletions
diff --git a/res/raw/preloaded_contacts.json b/res/raw/preloaded_contacts.json
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/res/raw/preloaded_contacts.json
diff --git a/res/raw/preloaded_contacts_schema.json b/res/raw/preloaded_contacts_schema.json
new file mode 100644
index 0000000..384d87f
--- /dev/null
+++ b/res/raw/preloaded_contacts_schema.json
@@ -0,0 +1,59 @@
+/*
+ This file encodes the contact information that must be preloaded into
+ the contacts database. The contact information is encoded according
+ to the schema outlined below.
+
+ The top-level object is an array of 'contacts'. Each of the array elements
+ is a contact with an array called 'data' defined within. Each of the objects
+ within 'data' objects describe an aspect of the contact - name, phone number,
+ address etc. Each aspect becomes a row in the raw_contacts table within
+ the contacts database.
+
+ When describing a property for a contact aspect, the keys and values can
+ reference java fields. These fields will be resolved at runtime. This enables
+ us to leverage the fields defined in android.provider.ContactsContract.* classes
+ to describe the contact information.
+
+ Note that if any java fields are referenced, they must be fully qualified names
+ as seen from a ClassLoader's perspective. There is no validation done to ensure
+ that the fields names can be resolved till runtime.
+
+ Conveniences afforded while declaring contact information :
+ @ = android.provider.ContactsContract$CommonDataKinds
+ @mimetype = android.provider.ContactsContract$Data.MIMETYPE
+
+ Example :
+ {
+ "contacts": [
+ {
+ "data": [
+ {
+ "@mimetype": "{{@$StructuredName.CONTENT_ITEM_TYPE}}",
+ "@$StructuredName.DISPLAY_NAME": "John Doe"
+ },
+
+ {
+ "@mimetype": "{{@$Phone.CONTENT_ITEM_TYPE}}",
+ "@$Phone.NUMBER": "123-456-7890",
+ "@$Phone.TYPE": "{{@$Phone.TYPE_WORK}}"
+ }
+ ]
+ }
+ ]
+ }
+
+ Property values can be static values or expressions that need to be evaluated, like
+ the property keys. Values are interpolated when enclosed within
+ double-curly-braces - '{{' - akin to a templating system like Handlebars.
+
+ Limitations :
+ - currently, for property values, there is no provision to embed an
+ expression-to-be-evaluated, within a larger string
+ ex. "{{com.example.foo.BAR}} additional content" isn't valid
+
+ - lack of compile time validation of the java fields specified or of the syntax
+
+ - lack of compile time validation of the property keys' with-respect-to the
+ columns of the raw_contacts table
+
+*/ \ No newline at end of file
diff --git a/res/values/config.xml b/res/values/config.xml
index 3707bcc..4c97cb8 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -33,4 +33,7 @@
<!-- If true, it supports fuzzy search for contacts number -->
<bool name="phone_number_fuzzy_search">false</bool>
+ <!-- request to attempt preload-ing contacts -->
+ <bool name="config_preload_contacts">false</bool>
+
</resources>
diff --git a/src/com/android/providers/contacts/Constants.java b/src/com/android/providers/contacts/Constants.java
index 8cf28e6..f828d74 100644
--- a/src/com/android/providers/contacts/Constants.java
+++ b/src/com/android/providers/contacts/Constants.java
@@ -21,4 +21,7 @@ public class Constants {
// Log tag for performance measurement.
// To enable: adb shell setprop log.tag.ContactsPerf VERBOSE
public static final String PERFORMANCE_TAG = "ContactsPerf";
+
+ // log info while preloading `default` contacts
+ public static final String TAG_DEBUG_PRELOAD_CONTACTS = "PreloadContacts";
}
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 55bae54..3bc0d9f 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -158,6 +158,7 @@ import com.android.providers.contacts.database.MoreDatabaseUtils;
import com.android.providers.contacts.util.Clock;
import com.android.providers.contacts.util.ContactsPermissions;
import com.android.providers.contacts.util.DbQueryUtils;
+import com.android.providers.contacts.util.PreloadedContactsFileParser;
import com.android.providers.contacts.util.NeededForTesting;
import com.android.providers.contacts.util.UserUtils;
import com.android.vcard.VCardComposer;
@@ -175,6 +176,7 @@ import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
@@ -193,6 +195,8 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
+import org.json.JSONException;
+
/**
* Contacts content provider. The contract between this provider and applications
* is defined in {@link ContactsContract}.
@@ -240,12 +244,15 @@ public class ContactsProvider2 extends AbstractContactsProvider
private static final int BACKGROUND_TASK_CHANGE_LOCALE = 9;
private static final int BACKGROUND_TASK_CLEANUP_PHOTOS = 10;
private static final int BACKGROUND_TASK_CLEAN_DELETE_LOG = 11;
+ private static final int BACKGROUND_TASK_ADD_DEFAULT_CONTACT = 12;
protected static final int STATUS_NORMAL = 0;
protected static final int STATUS_UPGRADING = 1;
protected static final int STATUS_CHANGING_LOCALE = 2;
protected static final int STATUS_NO_ACCOUNTS_NO_CONTACTS = 3;
+ private static final String PREF_PRELOADED_CONTACTS_ADDED = "preloaded_contacts_added";
+
/** Default for the maximum number of returned aggregation suggestions. */
private static final int DEFAULT_MAX_SUGGESTIONS = 5;
@@ -1565,6 +1572,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
scheduleBackgroundTask(BACKGROUND_TASK_OPEN_WRITE_ACCESS);
scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS);
scheduleBackgroundTask(BACKGROUND_TASK_CLEAN_DELETE_LOG);
+ scheduleBackgroundTask(BACKGROUND_TASK_ADD_DEFAULT_CONTACT);
return true;
}
@@ -1804,9 +1812,53 @@ public class ContactsProvider2 extends AbstractContactsProvider
DeletedContactsTableUtil.deleteOldLogs(db);
break;
}
+
+ case BACKGROUND_TASK_ADD_DEFAULT_CONTACT: {
+ if (shouldAttemptPreloadingContacts()) {
+ try {
+ InputStream inputStream = getContext().getResources().openRawResource(
+ R.raw.preloaded_contacts);
+ PreloadedContactsFileParser pcfp = new
+ PreloadedContactsFileParser(inputStream);
+ ArrayList<ContentProviderOperation> cpOperations = pcfp.parseForContacts();
+ if (cpOperations == null) break;
+
+ getContext().getContentResolver().applyBatch(ContactsContract.AUTHORITY,
+ cpOperations);
+ // persist the completion of the transaction
+ onPreloadingContactsComplete();
+
+ } catch (NotFoundException nfe) {
+ System.out.println();
+ nfe.printStackTrace();
+ } catch (JSONException e) {
+ e.printStackTrace();
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ } catch (OperationApplicationException e) {
+ e.printStackTrace();
+ }
+ }
+
+ break;
+ }
+
}
}
+ private boolean shouldAttemptPreloadingContacts() {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
+ return getContext().getResources().getBoolean(R.bool.config_preload_contacts) &&
+ !prefs.getBoolean(PREF_PRELOADED_CONTACTS_ADDED, false);
+ }
+
+ private void onPreloadingContactsComplete() {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(PREF_PRELOADED_CONTACTS_ADDED, true);
+ editor.commit();
+ }
+
public void onLocaleChanged() {
if (mProviderStatus != STATUS_NORMAL
&& mProviderStatus != STATUS_NO_ACCOUNTS_NO_CONTACTS) {
diff --git a/src/com/android/providers/contacts/util/PreloadedContactsFileParser.java b/src/com/android/providers/contacts/util/PreloadedContactsFileParser.java
new file mode 100644
index 0000000..ad9d0c0
--- /dev/null
+++ b/src/com/android/providers/contacts/util/PreloadedContactsFileParser.java
@@ -0,0 +1,225 @@
+package com.android.providers.contacts.util;
+
+import android.content.ContentProviderOperation;
+import android.provider.ContactsContract;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.providers.contacts.Constants;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Responsible for parsing the preloaded_contacts.json file and helping generate database commands
+ * to persist that information.
+ *
+ * Details about the json schema and encoding specification for the properties can be found in
+ * the preloaded_contacts_schema.json file under 'res/raw'
+ */
+public class PreloadedContactsFileParser {
+
+ private static final String TAG = "PreloadContacts";
+
+ private static String TOKEN_AT = "@";
+ private static String TOKEN_AT_SUB = "android.provider.ContactsContract$CommonDataKinds";
+ private static String TOKEN_CONTACTS_ROOT = "contacts";
+ private static String TOKEN_CONTACT_DATA = "data";
+ private static String TOKEN_MIMETYPE = "@mimetype";
+ private static String TOKEN_MIMETYPE_SUB = "android.provider.ContactsContract$Data.MIMETYPE";
+
+ private static Character CLASS_NAME_DELIMITER = '.';
+
+ private static Pattern mExpressionPattern = Pattern.compile("\\{\\{(.+?)\\}\\}");
+
+ private boolean mDebug;
+ private JSONObject mJsonRoot;
+ private HashMap<String,String> mResolvedNameCache;
+
+ public PreloadedContactsFileParser(InputStream inputStream) throws JSONException {
+ mDebug = Log.isLoggable(Constants.TAG_DEBUG_PRELOAD_CONTACTS, Log.DEBUG);
+ String jsonString = convertInputStreamToString(inputStream);
+ mJsonRoot = new JSONObject(jsonString);
+ mResolvedNameCache = new HashMap<String, String>();
+ }
+
+ private String convertInputStreamToString(InputStream is) {
+ BufferedReader br = new BufferedReader(new InputStreamReader(is));
+ StringBuilder sb = new StringBuilder();
+
+ String line = null;
+ try {
+ while ((line = br.readLine()) != null) {
+ sb.append(line).append('\n');
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ is.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Parses the json object and creates the necessary {@link ContentProviderOperation}s to
+ * construct the contacts specified
+ */
+ public ArrayList<ContentProviderOperation> parseForContacts() {
+ try {
+ ArrayList<ContentProviderOperation> cpOps = new ArrayList<ContentProviderOperation>();
+ JSONArray contacts = mJsonRoot.getJSONArray(TOKEN_CONTACTS_ROOT);
+ int numContacts = contacts.length();
+
+ for (int i = 0; i < numContacts; ++i) {
+ JSONArray contactData = contacts.getJSONObject(i).getJSONArray(TOKEN_CONTACT_DATA);
+ int rawEntries = contactData.length();
+
+ // create a new raw contact entry
+ cpOps.add(
+ ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
+ .build() );
+ int cvBackRef = cpOps.size() - 1;
+
+ for (int j = 0; j < rawEntries; ++j) {
+ JSONObject rawEntry = contactData.getJSONObject(j);
+ Iterator<String> keys = rawEntry.keys();
+
+ // build a ContentProviderOperation to add the contact's raw entry
+ ContentProviderOperation.Builder cpoBuilder =
+ ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
+ cpoBuilder.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID,
+ cvBackRef);
+
+ while (keys.hasNext()) {
+ String key = keys.next();
+ String value = rawEntry.getString(key);
+
+ if (mDebug) {
+ Log.d(TAG, "parsing property : " + key);
+ Log.d(TAG, "parsing property value : " + value);
+ }
+
+ String resolvedKey = null;
+ // keys always need interpolation
+ resolvedKey = resolvePropertyName(key);
+ // determine if the property is an expression that need to be evaluated
+ String resolvedValue = value;
+ Matcher matcher = mExpressionPattern.matcher(value);
+ if (matcher.matches()) {
+ matcher.reset();
+ matcher.find();
+ resolvedValue = resolvePropertyName(matcher.group(1));
+ }
+
+ if (mDebug) {
+ Log.d(TAG, "resolved property name : " + resolvedKey);
+ Log.d(TAG, "resolved property value : " + resolvedValue);
+ }
+
+ if (TextUtils.isEmpty(resolvedKey) || TextUtils.isEmpty(resolvedValue)) {
+ // don't persist this raw_contact value
+ continue;
+ } else {
+ cpoBuilder.withValue(resolvedKey, resolvedValue);
+ }
+
+ }
+
+ cpOps.add(cpoBuilder.build());
+ }
+
+ }
+ return cpOps;
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ /**
+ * parses an object's property name to determine its codified value
+ */
+ private String resolvePropertyName(String encodedName) {
+ if (TextUtils.isEmpty(encodedName)) {
+ return null;
+ }
+
+ if (mResolvedNameCache.containsKey(encodedName)) {
+ return mResolvedNameCache.get(encodedName);
+ }
+
+ String unwrappedName = encodedName;
+ // check if any substitution rules apply
+ if (TextUtils.equals(TOKEN_MIMETYPE, encodedName)) {
+ unwrappedName = TOKEN_MIMETYPE_SUB;
+ } else if (encodedName.startsWith(TOKEN_AT)) {
+ unwrappedName = encodedName.replace(TOKEN_AT, TOKEN_AT_SUB);
+ }
+
+ if (mDebug) {
+ Log.d(TAG, "encoded property name : " + encodedName);
+ Log.d(TAG, "resolved property name : " + unwrappedName);
+ }
+
+ String resolvedName = resolveCodifiedName(unwrappedName);
+ mResolvedNameCache.put(encodedName, resolvedName);
+ return resolvedName;
+ }
+
+ /**
+ * returns the string-ified value of the Java field the property name points to
+ */
+ private String resolveCodifiedName(String absoluteName) {
+ int delimiterIndex = TextUtils.lastIndexOf(absoluteName, CLASS_NAME_DELIMITER);
+ // ensure there is a field identifier to read
+ if (delimiterIndex == -1 || delimiterIndex >= absoluteName.length() - 1) {
+ return null;
+ }
+
+ String className = TextUtils.substring(absoluteName, 0, delimiterIndex);
+ String fieldName = absoluteName.substring(delimiterIndex + 1);
+
+ if (mDebug) {
+ Log.d(TAG, "property's class : " + className);
+ Log.d(TAG, "property's field : " + fieldName);
+ }
+
+ try {
+ Class clazz = Class.forName(className);
+ Field field = clazz.getField(fieldName);
+ String fieldValue = field.get(clazz).toString();
+ if (mDebug) {
+ Log.d(TAG, "fully resolved property name : " + fieldValue);
+ }
+ return fieldValue;
+
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+}