1 package com.android.car.messenger; 2 3 4 import android.Manifest; 5 import android.app.AppOpsManager; 6 import android.content.ContentResolver; 7 import android.content.ContentValues; 8 import android.content.Context; 9 import android.content.pm.PackageManager; 10 import android.database.Cursor; 11 import android.database.DatabaseUtils; 12 import android.net.Uri; 13 import android.provider.BaseColumns; 14 import android.provider.ContactsContract; 15 import android.provider.Telephony; 16 import android.text.TextUtils; 17 import android.util.Log; 18 19 import androidx.core.content.ContextCompat; 20 21 import com.android.car.messenger.common.Message; 22 import com.android.car.messenger.log.L; 23 24 import java.text.SimpleDateFormat; 25 import java.util.Date; 26 27 /** 28 * Reads and writes SMS Messages into the Telephony.SMS Database. 29 */ 30 class SmsDatabaseHandler { 31 private static final String TAG = "CM.SmsDatabaseHandler"; 32 private static final int MESSAGE_NOT_FOUND = -1; 33 private static final int DUPLICATE_MESSAGES_FOUND = -2; 34 private static final int DATABASE_ERROR = -3; 35 private static final Uri SMS_URI = Telephony.Sms.CONTENT_URI; 36 private static final String SMS_SELECTION = Telephony.Sms.ADDRESS + "=? AND " 37 + Telephony.Sms.BODY + "=? AND (" + Telephony.Sms.DATE + ">=? OR " + Telephony.Sms.DATE 38 + "<=?)"; 39 private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat( 40 "MMM dd,yyyy HH:mm"); 41 42 private final ContentResolver mContentResolver; 43 private final boolean mCanWriteToDatabase; 44 SmsDatabaseHandler(Context context)45 protected SmsDatabaseHandler(Context context) { 46 mCanWriteToDatabase = canWriteToDatabase(context); 47 mContentResolver = context.getContentResolver(); 48 readDatabase(context); 49 } 50 addOrUpdate(String deviceAddress, Message message)51 protected void addOrUpdate(String deviceAddress, Message message) { 52 if (!mCanWriteToDatabase) { 53 return; 54 } 55 56 int messageIndex = findMessageIndex(deviceAddress, message); 57 switch(messageIndex) { 58 case DUPLICATE_MESSAGES_FOUND: 59 removePreviousAndInsert(deviceAddress, message); 60 L.d(TAG, "Message has more than one duplicate in Telephony Database: %s", 61 message.toString()); 62 return; 63 case MESSAGE_NOT_FOUND: 64 mContentResolver.insert(SMS_URI, buildMessageContentValues(deviceAddress, message)); 65 return; 66 case DATABASE_ERROR: 67 return; 68 default: 69 update(messageIndex, buildMessageContentValues(deviceAddress, message)); 70 } 71 } 72 removeMessagesForDevice(String address)73 protected void removeMessagesForDevice(String address) { 74 if (!mCanWriteToDatabase) { 75 return; 76 } 77 78 String smsSelection = Telephony.Sms.ADDRESS + "=?"; 79 String[] smsSelectionArgs = {address}; 80 mContentResolver.delete(SMS_URI, smsSelection, smsSelectionArgs); 81 } 82 83 /** 84 * Reads the Telephony SMS Database, and logs all of the SMS messages that have been received 85 * in the last five minutes. 86 * @param context 87 */ readDatabase(Context context)88 protected static void readDatabase(Context context) { 89 if (!Log.isLoggable(TAG, Log.DEBUG)) { 90 return; 91 } 92 93 Long beginningTimeStamp = System.currentTimeMillis() - 300000; 94 String timeStamp = DATE_FORMATTER.format(new Date(beginningTimeStamp)); 95 Log.d(TAG, 96 " ------ printing SMSs received after " + timeStamp + "-------- "); 97 98 String smsSelection = Telephony.Sms.DATE + ">=?"; 99 String[] smsSelectionArgs = {Long.toString(beginningTimeStamp)}; 100 Cursor cursor = context.getContentResolver().query(SMS_URI, null, 101 smsSelection, 102 smsSelectionArgs, null /* sortOrder */); 103 if (cursor != null) { 104 while (cursor.moveToNext()) { 105 String body = cursor.getString(12); 106 107 Date date = new Date(cursor.getLong(4)); 108 Log.d(TAG, 109 "_id " + cursor.getInt(0) + " person: " + cursor.getInt(3) + " body: " 110 + body.substring(0, Math.min(body.length(), 17)) + " address: " 111 + cursor.getString(2) + " date: " + DATE_FORMATTER.format( 112 date) + " longDate " + cursor.getLong(4) + " read: " 113 + cursor.getInt(7)); 114 } 115 } 116 Log.d(TAG, " ------ end read table --------"); 117 } 118 119 /** Removes multiple previous copies, and inserts the new message. **/ removePreviousAndInsert(String deviceAddress, Message message)120 private void removePreviousAndInsert(String deviceAddress, Message message) { 121 String[] smsSelectionArgs = createSmsSelectionArgs(deviceAddress, message); 122 123 mContentResolver.delete(SMS_URI, SMS_SELECTION, smsSelectionArgs); 124 mContentResolver.insert(SMS_URI, buildMessageContentValues(deviceAddress, message)); 125 } 126 findMessageIndex(String deviceAddress, Message message)127 private int findMessageIndex(String deviceAddress, Message message) { 128 String[] smsSelectionArgs = createSmsSelectionArgs(deviceAddress, message); 129 130 String[] projection = {BaseColumns._ID}; 131 Cursor cursor = mContentResolver.query(SMS_URI, projection, SMS_SELECTION, 132 smsSelectionArgs, null /* sortOrder */); 133 134 if (cursor != null && cursor.getCount() != 0) { 135 if (cursor.moveToFirst() && cursor.isLast()) { 136 return getIdOrThrow(cursor); 137 } else { 138 return DUPLICATE_MESSAGES_FOUND; 139 } 140 } else { 141 return MESSAGE_NOT_FOUND; 142 } 143 } 144 getIdOrThrow(Cursor cursor)145 private int getIdOrThrow(Cursor cursor) { 146 try { 147 int columnIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID); 148 return cursor.getInt(columnIndex); 149 } catch (IllegalArgumentException e) { 150 L.d(TAG, "Could not find _id column: " + e.getMessage()); 151 return DATABASE_ERROR; 152 } 153 } 154 update(int messageIndex, ContentValues value)155 private void update(int messageIndex, ContentValues value) { 156 final String smsSelection = BaseColumns._ID + "=?"; 157 String[] smsSelectionArgs = {Integer.toString(messageIndex)}; 158 159 mContentResolver.update(SMS_URI, value, smsSelection, smsSelectionArgs); 160 } 161 162 /** Create the ContentValues object using message info, following SMS columns **/ buildMessageContentValues(String deviceAddress, Message message)163 private ContentValues buildMessageContentValues(String deviceAddress, Message message) { 164 ContentValues newMessage = new ContentValues(); 165 newMessage.put(Telephony.Sms.BODY, DatabaseUtils.sqlEscapeString(message.getMessageText())); 166 newMessage.put(Telephony.Sms.DATE, message.getReceivedTime()); 167 newMessage.put(Telephony.Sms.ADDRESS, deviceAddress); 168 // TODO: if contactId is null, add it. 169 newMessage.put(Telephony.Sms.PERSON, 170 getContactId(mContentResolver, 171 message.getSenderContactUri())); 172 newMessage.put(Telephony.Sms.READ, (message.isReadOnPhone() 173 || message.shouldExcludeFromNotification())); 174 return newMessage; 175 } 176 createSmsSelectionArgs(String deviceAddress, Message message)177 private String[] createSmsSelectionArgs(String deviceAddress, Message message) { 178 String sqlFriendlyMessageText = DatabaseUtils.sqlEscapeString(message.getMessageText()); 179 String[] smsSelectionArgs = {deviceAddress, sqlFriendlyMessageText, 180 Long.toString(message.getReceivedTime() - 5000), Long.toString( 181 message.getReceivedTime() + 5000)}; 182 return smsSelectionArgs; 183 } 184 185 /** Checks if the application has the needed AppOps permission to write to the Telephony DB. **/ canWriteToDatabase(Context context)186 private boolean canWriteToDatabase(Context context) { 187 boolean granted = ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_SMS) 188 == PackageManager.PERMISSION_GRANTED; 189 190 AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); 191 int mode = appOps.checkOpNoThrow(AppOpsManager.OP_WRITE_SMS, android.os.Process.myUid(), 192 context.getPackageName()); 193 if (mode != AppOpsManager.MODE_DEFAULT) { 194 granted = (mode == AppOpsManager.MODE_ALLOWED); 195 } 196 197 return granted; 198 } 199 200 // TODO: move out to a shared library. getContactId(ContentResolver cr, String contactUri)201 private static int getContactId(ContentResolver cr, String contactUri) { 202 if (TextUtils.isEmpty(contactUri)) { 203 return 0; 204 } 205 206 Uri lookupUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, 207 Uri.encode(contactUri)); 208 String[] projection = new String[]{ContactsContract.PhoneLookup._ID}; 209 210 try (Cursor cursor = cr.query(lookupUri, projection, null, null, null)) { 211 if (cursor != null && cursor.moveToFirst() && cursor.isLast()) { 212 return cursor.getInt(cursor.getColumnIndex(ContactsContract.PhoneLookup._ID)); 213 } else { 214 L.w(TAG, "Unable to find contact id from phone number."); 215 } 216 } 217 218 return 0; 219 } 220 } 221