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
17package com.android.mms.util;
18
19import android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.Context;
22import android.content.SharedPreferences;
23import android.database.Cursor;
24import android.database.sqlite.SqliteWrapper;
25import android.net.Uri;
26import android.preference.PreferenceManager;
27import android.provider.BaseColumns;
28import android.provider.Telephony;
29import android.provider.Telephony.Mms;
30import android.provider.Telephony.Sms;
31import android.provider.Telephony.Sms.Conversations;
32import android.util.Log;
33
34import com.android.mms.MmsConfig;
35import com.android.mms.ui.MessageUtils;
36import com.android.mms.ui.MessagingPreferenceActivity;
37
38/**
39 * The recycler is responsible for deleting old messages.
40 */
41public 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