• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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