/* * 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.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. * */ 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 recipients = new HashSet(); 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. * *

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.

*/ private static long getOrCreateThreadIdInternal(Context context, Set 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; } }