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