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.LogTag;
35import com.android.mms.MmsConfig;
36import com.android.mms.ui.MessageUtils;
37import com.android.mms.ui.MessagingPreferenceActivity;
38
39/**
40 * The recycler is responsible for deleting old messages.
41 */
42public 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