1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.messaging.sms; 18 19 import android.content.Context; 20 import android.database.Cursor; 21 import android.net.Uri; 22 import android.provider.BaseColumns; 23 import android.text.TextUtils; 24 import android.util.Patterns; 25 26 import com.android.messaging.mmslib.SqliteWrapper; 27 import com.android.messaging.util.LogUtil; 28 29 import java.util.HashSet; 30 import java.util.Set; 31 import java.util.regex.Matcher; 32 import java.util.regex.Pattern; 33 34 /** 35 * Utility functions for the Messaging Service 36 */ 37 public class MmsSmsUtils { MmsSmsUtils()38 private MmsSmsUtils() { 39 // Forbidden being instantiated. 40 } 41 42 // An alias (or commonly called "nickname") is: 43 // Nickname must begin with a letter. 44 // Only letters a-z, numbers 0-9, or . are allowed in Nickname field. isAlias(final String string, final int subId)45 public static boolean isAlias(final String string, final int subId) { 46 if (!MmsConfig.get(subId).isAliasEnabled()) { 47 return false; 48 } 49 50 final int len = string == null ? 0 : string.length(); 51 52 if (len < MmsConfig.get(subId).getAliasMinChars() || 53 len > MmsConfig.get(subId).getAliasMaxChars()) { 54 return false; 55 } 56 57 if (!Character.isLetter(string.charAt(0))) { // Nickname begins with a letter 58 return false; 59 } 60 for (int i = 1; i < len; i++) { 61 final char c = string.charAt(i); 62 if (!(Character.isLetterOrDigit(c) || c == '.')) { 63 return false; 64 } 65 } 66 67 return true; 68 } 69 70 /** 71 * mailbox = name-addr 72 * name-addr = [display-name] angle-addr 73 * angle-addr = [CFWS] "<" addr-spec ">" [CFWS] 74 */ 75 public static final Pattern NAME_ADDR_EMAIL_PATTERN = 76 Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*"); 77 extractAddrSpec(final String address)78 public static String extractAddrSpec(final String address) { 79 final Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(address); 80 81 if (match.matches()) { 82 return match.group(2); 83 } 84 return address; 85 } 86 87 /** 88 * Returns true if the address is an email address 89 * 90 * @param address the input address to be tested 91 * @return true if address is an email address 92 */ isEmailAddress(final String address)93 public static boolean isEmailAddress(final String address) { 94 if (TextUtils.isEmpty(address)) { 95 return false; 96 } 97 98 final String s = extractAddrSpec(address); 99 final Matcher match = Patterns.EMAIL_ADDRESS.matcher(s); 100 return match.matches(); 101 } 102 103 /** 104 * This pattern is intended for searching for carrier specific phone numbers starting with star 105 * sign and digits such as the voice mail number. 106 * (e.g. *20 Chile Claro) 107 */ 108 private static final Pattern PHONE_NUMBER_STARTING_WITH_STAR_PATTERN = 109 Pattern.compile("\\*[0-9]+"); 110 111 /** 112 * Returns true if the number is a Phone number 113 * 114 * @param number the input number to be tested 115 * @return true if number is a Phone number 116 */ isPhoneNumber(final String number)117 public static boolean isPhoneNumber(final String number) { 118 if (TextUtils.isEmpty(number)) { 119 return false; 120 } 121 122 Matcher match = Patterns.PHONE.matcher(number); 123 if (!match.matches()) { 124 match = PHONE_NUMBER_STARTING_WITH_STAR_PATTERN.matcher(number); 125 return match.matches(); 126 } 127 return true; 128 } 129 130 /** 131 * Check if MMS is required when sending to email address 132 * 133 * @param destinationHasEmailAddress destination includes an email address 134 * @return true if MMS is required. 135 */ getRequireMmsForEmailAddress(final boolean destinationHasEmailAddress, final int subId)136 public static boolean getRequireMmsForEmailAddress(final boolean destinationHasEmailAddress, 137 final int subId) { 138 if (!TextUtils.isEmpty(MmsConfig.get(subId).getEmailGateway())) { 139 return false; 140 } else { 141 return destinationHasEmailAddress; 142 } 143 } 144 145 /** 146 * Helper functions for the "threads" table used by MMS and SMS. 147 */ 148 public static final class Threads implements android.provider.Telephony.ThreadsColumns { 149 private static final String[] ID_PROJECTION = { BaseColumns._ID }; 150 private static final Uri THREAD_ID_CONTENT_URI = Uri.parse( 151 "content://mms-sms/threadID"); 152 public static final Uri CONTENT_URI = Uri.withAppendedPath( 153 android.provider.Telephony.MmsSms.CONTENT_URI, "conversations"); 154 155 // No one should construct an instance of this class. Threads()156 private Threads() { 157 } 158 159 /** 160 * This is a single-recipient version of 161 * getOrCreateThreadId. It's convenient for use with SMS 162 * messages. 163 */ getOrCreateThreadId(final Context context, final String recipient)164 public static long getOrCreateThreadId(final Context context, final String recipient) { 165 final Set<String> recipients = new HashSet<String>(); 166 167 recipients.add(recipient); 168 return getOrCreateThreadId(context, recipients); 169 } 170 171 /** 172 * Given the recipients list and subject of an unsaved message, 173 * return its thread ID. If the message starts a new thread, 174 * allocate a new thread ID. Otherwise, use the appropriate 175 * existing thread ID. 176 * 177 * Find the thread ID of the same set of recipients (in 178 * any order, without any additions). If one 179 * is found, return it. Otherwise, return a unique thread ID. 180 */ getOrCreateThreadId( final Context context, final Set<String> recipients)181 public static long getOrCreateThreadId( 182 final Context context, final Set<String> recipients) { 183 final Uri.Builder uriBuilder = THREAD_ID_CONTENT_URI.buildUpon(); 184 185 for (String recipient : recipients) { 186 if (isEmailAddress(recipient)) { 187 recipient = extractAddrSpec(recipient); 188 } 189 190 uriBuilder.appendQueryParameter("recipient", recipient); 191 } 192 193 final Uri uri = uriBuilder.build(); 194 //if (DEBUG) Rlog.v(TAG, "getOrCreateThreadId uri: " + uri); 195 196 final Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(), 197 uri, ID_PROJECTION, null, null, null); 198 if (cursor != null) { 199 try { 200 if (cursor.moveToFirst()) { 201 return cursor.getLong(0); 202 } else { 203 LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, 204 "getOrCreateThreadId returned no rows!"); 205 } 206 } finally { 207 cursor.close(); 208 } 209 } 210 211 LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "getOrCreateThreadId failed with " 212 + LogUtil.sanitizePII(recipients.toString())); 213 throw new IllegalArgumentException("Unable to find or allocate a thread ID."); 214 } 215 } 216 } 217