Backport Telephony.Threads to enable SMS interaction (1/2)

Also refactored private methods in PhoneNumberUtilsCompat.

Bug: 25629359

Change-Id: I9ac27050e35860ab2b35cc02faad23a88191457e
diff --git a/src/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java b/src/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java
index abf7257..3cdb127 100644
--- a/src/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java
+++ b/src/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java
@@ -42,7 +42,7 @@
                 >= Build.VERSION_CODES.M) {
             return PhoneNumberUtils.createTtsSpannable(phoneNumber);
         } else {
-            return createTtsSpannablePrivate(phoneNumber);
+            return createTtsSpannableInternal(phoneNumber);
         }
     }
 
@@ -51,14 +51,14 @@
                 >= Build.VERSION_CODES.M) {
             return PhoneNumberUtils.createTtsSpan(phoneNumber);
         } else {
-            return createTtsSpanPrivate(phoneNumber);
+            return createTtsSpanInternal(phoneNumber);
         }
     }
 
     /**
      * Copied from {@link PhoneNumberUtils#createTtsSpannable}
      */
-    private static CharSequence createTtsSpannablePrivate(CharSequence phoneNumber) {
+    private static CharSequence createTtsSpannableInternal(CharSequence phoneNumber) {
         if (phoneNumber == null) {
             return null;
         }
@@ -85,7 +85,7 @@
     /**
      * Copied from {@link PhoneNumberUtils#createTtsSpan}
      */
-    private static TtsSpan createTtsSpanPrivate(String phoneNumberString) {
+    private static TtsSpan createTtsSpanInternal(String phoneNumberString) {
         if (phoneNumberString == null) {
             return null;
         }
diff --git a/src/com/android/contacts/common/compat/TelephonyThreadsCompat.java b/src/com/android/contacts/common/compat/TelephonyThreadsCompat.java
new file mode 100644
index 0000000..d9642c7
--- /dev/null
+++ b/src/com/android/contacts/common/compat/TelephonyThreadsCompat.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2015 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.contacts.common.compat;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.BaseColumns;
+import android.provider.Telephony;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Patterns;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This class contains static utility methods and variables extracted from Telephony and
+ * SqliteWrapper, and the methods were made visible in API level 23. In this way, we could
+ * enable the corresponding functionality for pre-M devices. We need maintain this class and keep
+ * it synced with Telephony and SqliteWrapper.
+ */
+public class TelephonyThreadsCompat {
+    /**
+     * Not instantiable.
+     */
+    private TelephonyThreadsCompat() {}
+
+    private static final String TAG = "TelephonyThreadsCompat";
+
+    public static long getOrCreateThreadId(Context context, String recipient) {
+        if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) {
+            return Telephony.Threads.getOrCreateThreadId(context, recipient);
+        } else {
+            return getOrCreateThreadIdInternal(context, recipient);
+        }
+    }
+
+    // Below is code copied from Telephony and SqliteWrapper
+    /**
+     * Private {@code content://} style URL for this table. Used by
+     * {@link #getOrCreateThreadId(Context, Set)}.
+     */
+    private static final Uri THREAD_ID_CONTENT_URI = Uri.parse("content://mms-sms/threadID");
+
+    private static final String[] ID_PROJECTION = { BaseColumns._ID };
+
+    /**
+     * Regex pattern for names and email addresses.
+     * <ul>
+     *     <li><em>mailbox</em> = {@code name-addr}</li>
+     *     <li><em>name-addr</em> = {@code [display-name] angle-addr}</li>
+     *     <li><em>angle-addr</em> = {@code [CFWS] "<" addr-spec ">" [CFWS]}</li>
+     * </ul>
+     */
+    private static final Pattern NAME_ADDR_EMAIL_PATTERN =
+            Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*");
+
+    /**
+     * Copied from {@link Telephony.Threads#getOrCreateThreadId(Context, String)}
+     */
+    private static long getOrCreateThreadIdInternal(Context context, String recipient) {
+        Set<String> recipients = new HashSet<String>();
+
+        recipients.add(recipient);
+        return getOrCreateThreadIdInternal(context, recipients);
+    }
+
+    /**
+     * Given the recipients list and subject of an unsaved message,
+     * return its thread ID.  If the message starts a new thread,
+     * allocate a new thread ID.  Otherwise, use the appropriate
+     * existing thread ID.
+     *
+     * <p>Find the thread ID of the same set of recipients (in any order,
+     * without any additions). If one is found, return it. Otherwise,
+     * return a unique thread ID.</p>
+     */
+    private static long getOrCreateThreadIdInternal(Context context, Set<String> recipients) {
+        Uri.Builder uriBuilder = THREAD_ID_CONTENT_URI.buildUpon();
+
+        for (String recipient : recipients) {
+            if (isEmailAddress(recipient)) {
+                recipient = extractAddrSpec(recipient);
+            }
+
+            uriBuilder.appendQueryParameter("recipient", recipient);
+        }
+
+        Uri uri = uriBuilder.build();
+
+        Cursor cursor = query(
+                context.getContentResolver(), uri, ID_PROJECTION, null, null, null);
+        if (cursor != null) {
+            try {
+                if (cursor.moveToFirst()) {
+                    return cursor.getLong(0);
+                } else {
+                    Log.e(TAG, "getOrCreateThreadId returned no rows!");
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        Log.e(TAG, "getOrCreateThreadId failed with uri " + uri.toString());
+        throw new IllegalArgumentException("Unable to find or allocate a thread ID.");
+    }
+
+    /**
+     * Copied from {@link SqliteWrapper#query}
+     */
+    private static Cursor query(ContentResolver resolver, Uri uri, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder) {
+        try {
+            return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
+        } catch (Exception e) {
+            Log.e(TAG, "Catch an exception when query: ", e);
+            return null;
+        }
+    }
+
+    /**
+     * Is the specified address an email address?
+     *
+     * @param address the input address to test
+     * @return true if address is an email address; false otherwise.
+     */
+    private static boolean isEmailAddress(String address) {
+        if (TextUtils.isEmpty(address)) {
+            return false;
+        }
+
+        String s = extractAddrSpec(address);
+        Matcher match = Patterns.EMAIL_ADDRESS.matcher(s);
+        return match.matches();
+    }
+
+    /**
+     * Helper method to extract email address from address string.
+     */
+    private static String extractAddrSpec(String address) {
+        Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(address);
+
+        if (match.matches()) {
+            return match.group(2);
+        }
+        return address;
+    }
+}