1 /* 2 * Copyright (C) 2009 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.mms.util; 18 19 import android.content.ContentResolver; 20 import android.content.ContentUris; 21 import android.content.Context; 22 import android.content.SharedPreferences; 23 import android.database.Cursor; 24 import android.database.sqlite.SqliteWrapper; 25 import android.net.Uri; 26 import android.preference.PreferenceManager; 27 import android.provider.BaseColumns; 28 import android.provider.Telephony; 29 import android.provider.Telephony.Mms; 30 import android.provider.Telephony.Sms; 31 import android.provider.Telephony.Sms.Conversations; 32 import android.util.Log; 33 34 import com.android.mms.MmsConfig; 35 import com.android.mms.ui.MessageUtils; 36 import com.android.mms.ui.MessagingPreferenceActivity; 37 38 /** 39 * The recycler is responsible for deleting old messages. 40 */ 41 public abstract class Recycler { 42 private static final boolean LOCAL_DEBUG = false; 43 private static final String TAG = "Recycler"; 44 45 // Default preference values 46 private static final boolean DEFAULT_AUTO_DELETE = false; 47 48 private static SmsRecycler sSmsRecycler; 49 private static MmsRecycler sMmsRecycler; 50 51 public static SmsRecycler getSmsRecycler() { 52 if (sSmsRecycler == null) { 53 sSmsRecycler = new SmsRecycler(); 54 } 55 return sSmsRecycler; 56 } 57 58 public static MmsRecycler getMmsRecycler() { 59 if (sMmsRecycler == null) { 60 sMmsRecycler = new MmsRecycler(); 61 } 62 return sMmsRecycler; 63 } 64 65 public static boolean checkForThreadsOverLimit(Context context) { 66 Recycler smsRecycler = getSmsRecycler(); 67 Recycler mmsRecycler = getMmsRecycler(); 68 69 return smsRecycler.anyThreadOverLimit(context) || mmsRecycler.anyThreadOverLimit(context); 70 } 71 72 public void deleteOldMessages(Context context) { 73 if (LOCAL_DEBUG) { 74 Log.v(TAG, "Recycler.deleteOldMessages this: " + this); 75 } 76 if (!isAutoDeleteEnabled(context)) { 77 return; 78 } 79 80 Cursor cursor = getAllThreads(context); 81 try { 82 int limit = getMessageLimit(context); 83 while (cursor.moveToNext()) { 84 long threadId = getThreadId(cursor); 85 deleteMessagesForThread(context, threadId, limit); 86 } 87 } finally { 88 cursor.close(); 89 } 90 } 91 92 public void deleteOldMessagesByThreadId(Context context, long threadId) { 93 if (LOCAL_DEBUG) { 94 Log.v(TAG, "Recycler.deleteOldMessagesByThreadId this: " + this + 95 " threadId: " + threadId); 96 } 97 if (!isAutoDeleteEnabled(context)) { 98 return; 99 } 100 101 deleteMessagesForThread(context, threadId, getMessageLimit(context)); 102 } 103 104 public static boolean isAutoDeleteEnabled(Context context) { 105 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 106 return prefs.getBoolean(MessagingPreferenceActivity.AUTO_DELETE, 107 DEFAULT_AUTO_DELETE); 108 } 109 110 abstract public int getMessageLimit(Context context); 111 112 abstract public void setMessageLimit(Context context, int limit); 113 114 public int getMessageMinLimit() { 115 return MmsConfig.getMinMessageCountPerThread(); 116 } 117 118 public int getMessageMaxLimit() { 119 return MmsConfig.getMaxMessageCountPerThread(); 120 } 121 122 abstract protected long getThreadId(Cursor cursor); 123 124 abstract protected Cursor getAllThreads(Context context); 125 126 abstract protected void deleteMessagesForThread(Context context, long threadId, int keep); 127 128 abstract protected void dumpMessage(Cursor cursor, Context context); 129 130 abstract protected boolean anyThreadOverLimit(Context context); 131 132 public static class SmsRecycler extends Recycler { 133 private static final String[] ALL_SMS_THREADS_PROJECTION = { 134 Telephony.Sms.Conversations.THREAD_ID, 135 Telephony.Sms.Conversations.MESSAGE_COUNT 136 }; 137 138 private static final int ID = 0; 139 private static final int MESSAGE_COUNT = 1; 140 141 static private final String[] SMS_MESSAGE_PROJECTION = new String[] { 142 BaseColumns._ID, 143 Conversations.THREAD_ID, 144 Sms.ADDRESS, 145 Sms.BODY, 146 Sms.DATE, 147 Sms.READ, 148 Sms.TYPE, 149 Sms.STATUS, 150 }; 151 152 // The indexes of the default columns which must be consistent 153 // with above PROJECTION. 154 static private final int COLUMN_ID = 0; 155 static private final int COLUMN_THREAD_ID = 1; 156 static private final int COLUMN_SMS_ADDRESS = 2; 157 static private final int COLUMN_SMS_BODY = 3; 158 static private final int COLUMN_SMS_DATE = 4; 159 static private final int COLUMN_SMS_READ = 5; 160 static private final int COLUMN_SMS_TYPE = 6; 161 static private final int COLUMN_SMS_STATUS = 7; 162 163 private final String MAX_SMS_MESSAGES_PER_THREAD = "MaxSmsMessagesPerThread"; 164 165 public int getMessageLimit(Context context) { 166 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 167 return prefs.getInt(MAX_SMS_MESSAGES_PER_THREAD, 168 MmsConfig.getDefaultSMSMessagesPerThread()); 169 } 170 171 public void setMessageLimit(Context context, int limit) { 172 SharedPreferences.Editor editPrefs = 173 PreferenceManager.getDefaultSharedPreferences(context).edit(); 174 editPrefs.putInt(MAX_SMS_MESSAGES_PER_THREAD, limit); 175 editPrefs.apply(); 176 } 177 178 protected long getThreadId(Cursor cursor) { 179 return cursor.getLong(ID); 180 } 181 182 protected Cursor getAllThreads(Context context) { 183 ContentResolver resolver = context.getContentResolver(); 184 Cursor cursor = SqliteWrapper.query(context, resolver, 185 Telephony.Sms.Conversations.CONTENT_URI, 186 ALL_SMS_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER); 187 188 return cursor; 189 } 190 191 protected void deleteMessagesForThread(Context context, long threadId, int keep) { 192 if (LOCAL_DEBUG) { 193 Log.v(TAG, "SMS: deleteMessagesForThread"); 194 } 195 ContentResolver resolver = context.getContentResolver(); 196 Cursor cursor = null; 197 try { 198 cursor = SqliteWrapper.query(context, resolver, 199 ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId), 200 SMS_MESSAGE_PROJECTION, 201 "locked=0", 202 null, "date DESC"); // get in newest to oldest order 203 if (cursor == null) { 204 Log.e(TAG, "SMS: deleteMessagesForThread got back null cursor"); 205 return; 206 } 207 int count = cursor.getCount(); 208 int numberToDelete = count - keep; 209 if (LOCAL_DEBUG) { 210 Log.v(TAG, "SMS: deleteMessagesForThread keep: " + keep + 211 " count: " + count + 212 " numberToDelete: " + numberToDelete); 213 } 214 if (numberToDelete <= 0) { 215 return; 216 } 217 // Move to the keep limit and then delete everything older than that one. 218 cursor.move(keep); 219 long latestDate = cursor.getLong(COLUMN_SMS_DATE); 220 221 long cntDeleted = SqliteWrapper.delete(context, resolver, 222 ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId), 223 "locked=0 AND date<" + latestDate, 224 null); 225 if (LOCAL_DEBUG) { 226 Log.v(TAG, "SMS: deleteMessagesForThread cntDeleted: " + cntDeleted); 227 } 228 } finally { 229 if (cursor != null) { 230 cursor.close(); 231 } 232 } 233 } 234 235 protected void dumpMessage(Cursor cursor, Context context) { 236 long date = cursor.getLong(COLUMN_SMS_DATE); 237 String dateStr = MessageUtils.formatTimeStampString(context, date, true); 238 if (LOCAL_DEBUG) { 239 Log.v(TAG, "Recycler message " + 240 "\n address: " + cursor.getString(COLUMN_SMS_ADDRESS) + 241 "\n body: " + cursor.getString(COLUMN_SMS_BODY) + 242 "\n date: " + dateStr + 243 "\n date: " + date + 244 "\n read: " + cursor.getInt(COLUMN_SMS_READ)); 245 } 246 } 247 248 @Override 249 protected boolean anyThreadOverLimit(Context context) { 250 Cursor cursor = getAllThreads(context); 251 if (cursor == null) { 252 return false; 253 } 254 int limit = getMessageLimit(context); 255 try { 256 while (cursor.moveToNext()) { 257 long threadId = getThreadId(cursor); 258 ContentResolver resolver = context.getContentResolver(); 259 Cursor msgs = SqliteWrapper.query(context, resolver, 260 ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId), 261 SMS_MESSAGE_PROJECTION, 262 "locked=0", 263 null, "date DESC"); // get in newest to oldest order 264 if (msgs == null) { 265 return false; 266 } 267 try { 268 if (msgs.getCount() >= limit) { 269 return true; 270 } 271 } finally { 272 msgs.close(); 273 } 274 } 275 } finally { 276 cursor.close(); 277 } 278 return false; 279 } 280 } 281 282 public static class MmsRecycler extends Recycler { 283 private static final String[] ALL_MMS_THREADS_PROJECTION = { 284 "thread_id", "count(*) as msg_count" 285 }; 286 287 private static final int ID = 0; 288 private static final int MESSAGE_COUNT = 1; 289 290 static private final String[] MMS_MESSAGE_PROJECTION = new String[] { 291 BaseColumns._ID, 292 Conversations.THREAD_ID, 293 Mms.DATE, 294 }; 295 296 // The indexes of the default columns which must be consistent 297 // with above PROJECTION. 298 static private final int COLUMN_ID = 0; 299 static private final int COLUMN_THREAD_ID = 1; 300 static private final int COLUMN_MMS_DATE = 2; 301 static private final int COLUMN_MMS_READ = 3; 302 303 private final String MAX_MMS_MESSAGES_PER_THREAD = "MaxMmsMessagesPerThread"; 304 305 public int getMessageLimit(Context context) { 306 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 307 return prefs.getInt(MAX_MMS_MESSAGES_PER_THREAD, 308 MmsConfig.getDefaultMMSMessagesPerThread()); 309 } 310 311 public void setMessageLimit(Context context, int limit) { 312 SharedPreferences.Editor editPrefs = 313 PreferenceManager.getDefaultSharedPreferences(context).edit(); 314 editPrefs.putInt(MAX_MMS_MESSAGES_PER_THREAD, limit); 315 editPrefs.apply(); 316 } 317 318 protected long getThreadId(Cursor cursor) { 319 return cursor.getLong(ID); 320 } 321 322 protected Cursor getAllThreads(Context context) { 323 ContentResolver resolver = context.getContentResolver(); 324 Cursor cursor = SqliteWrapper.query(context, resolver, 325 Uri.withAppendedPath(Telephony.Mms.CONTENT_URI, "threads"), 326 ALL_MMS_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER); 327 328 return cursor; 329 } 330 331 public void deleteOldMessagesInSameThreadAsMessage(Context context, Uri uri) { 332 if (LOCAL_DEBUG) { 333 Log.v(TAG, "MMS: deleteOldMessagesByUri"); 334 } 335 if (!isAutoDeleteEnabled(context)) { 336 return; 337 } 338 Cursor cursor = null; 339 long latestDate = 0; 340 long threadId = 0; 341 try { 342 String msgId = uri.getLastPathSegment(); 343 ContentResolver resolver = context.getContentResolver(); 344 cursor = SqliteWrapper.query(context, resolver, 345 Telephony.Mms.CONTENT_URI, 346 MMS_MESSAGE_PROJECTION, 347 "thread_id in (select thread_id from pdu where _id=" + msgId + 348 ") AND locked=0", 349 null, "date DESC"); // get in newest to oldest order 350 if (cursor == null) { 351 Log.e(TAG, "MMS: deleteOldMessagesInSameThreadAsMessage got back null cursor"); 352 return; 353 } 354 355 int count = cursor.getCount(); 356 int keep = getMessageLimit(context); 357 int numberToDelete = count - keep; 358 if (LOCAL_DEBUG) { 359 Log.v(TAG, "MMS: deleteOldMessagesByUri keep: " + keep + 360 " count: " + count + 361 " numberToDelete: " + numberToDelete); 362 } 363 if (numberToDelete <= 0) { 364 return; 365 } 366 // Move to the keep limit and then delete everything older than that one. 367 cursor.move(keep); 368 latestDate = cursor.getLong(COLUMN_MMS_DATE); 369 threadId = cursor.getLong(COLUMN_THREAD_ID); 370 } finally { 371 if (cursor != null) { 372 cursor.close(); 373 } 374 } 375 if (threadId != 0) { 376 deleteMessagesOlderThanDate(context, threadId, latestDate); 377 } 378 } 379 380 protected void deleteMessagesForThread(Context context, long threadId, int keep) { 381 if (LOCAL_DEBUG) { 382 Log.v(TAG, "MMS: deleteMessagesForThread"); 383 } 384 if (threadId == 0) { 385 return; 386 } 387 Cursor cursor = null; 388 long latestDate = 0; 389 try { 390 ContentResolver resolver = context.getContentResolver(); 391 cursor = SqliteWrapper.query(context, resolver, 392 Telephony.Mms.CONTENT_URI, 393 MMS_MESSAGE_PROJECTION, 394 "thread_id=" + threadId + " AND locked=0", 395 null, "date DESC"); // get in newest to oldest order 396 if (cursor == null) { 397 Log.e(TAG, "MMS: deleteMessagesForThread got back null cursor"); 398 return; 399 } 400 401 int count = cursor.getCount(); 402 int numberToDelete = count - keep; 403 if (LOCAL_DEBUG) { 404 Log.v(TAG, "MMS: deleteMessagesForThread keep: " + keep + 405 " count: " + count + 406 " numberToDelete: " + numberToDelete); 407 } 408 if (numberToDelete <= 0) { 409 return; 410 } 411 // Move to the keep limit and then delete everything older than that one. 412 cursor.move(keep); 413 latestDate = cursor.getLong(COLUMN_MMS_DATE); 414 } finally { 415 if (cursor != null) { 416 cursor.close(); 417 } 418 } 419 deleteMessagesOlderThanDate(context, threadId, latestDate); 420 } 421 422 private void deleteMessagesOlderThanDate(Context context, long threadId, 423 long latestDate) { 424 long cntDeleted = SqliteWrapper.delete(context, context.getContentResolver(), 425 Telephony.Mms.CONTENT_URI, 426 "thread_id=" + threadId + " AND locked=0 AND date<" + latestDate, 427 null); 428 if (LOCAL_DEBUG) { 429 Log.v(TAG, "MMS: deleteMessagesOlderThanDate cntDeleted: " + cntDeleted); 430 } 431 } 432 433 protected void dumpMessage(Cursor cursor, Context context) { 434 long id = cursor.getLong(COLUMN_ID); 435 if (LOCAL_DEBUG) { 436 Log.v(TAG, "Recycler message " + 437 "\n id: " + id 438 ); 439 } 440 } 441 442 @Override 443 protected boolean anyThreadOverLimit(Context context) { 444 Cursor cursor = getAllThreads(context); 445 if (cursor == null) { 446 return false; 447 } 448 int limit = getMessageLimit(context); 449 try { 450 while (cursor.moveToNext()) { 451 long threadId = getThreadId(cursor); 452 ContentResolver resolver = context.getContentResolver(); 453 Cursor msgs = SqliteWrapper.query(context, resolver, 454 Telephony.Mms.CONTENT_URI, 455 MMS_MESSAGE_PROJECTION, 456 "thread_id=" + threadId + " AND locked=0", 457 null, "date DESC"); // get in newest to oldest order 458 459 if (msgs == null) { 460 return false; 461 } 462 try { 463 if (msgs.getCount() >= limit) { 464 return true; 465 } 466 } finally { 467 msgs.close(); 468 } 469 } 470 } finally { 471 cursor.close(); 472 } 473 return false; 474 } 475 } 476 477 } 478