From 4852f70aaa45078b6874c146d38bf872f6b6b509 Mon Sep 17 00:00:00 2001 From: Rohit Yengisetty Date: Thu, 8 Oct 2015 18:31:42 -0700 Subject: Add a richer system around preloading contacts Change-Id: I2c3b6b79ee41eb73948f9a053454654ee8566a12 --- src/com/android/providers/contacts/Constants.java | 3 + .../providers/contacts/ContactsProvider2.java | 52 +++++ .../contacts/util/PreloadedContactsFileParser.java | 225 +++++++++++++++++++++ 3 files changed, 280 insertions(+) create mode 100644 src/com/android/providers/contacts/util/PreloadedContactsFileParser.java (limited to 'src') 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 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 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(); + } + + 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 parseForContacts() { + try { + ArrayList cpOps = new ArrayList(); + 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 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; + } + +} -- cgit v1.1