1 /* 2 * Copyright (C) 2015 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.messaging.datamodel.action; 18 19 import android.database.Cursor; 20 import android.net.Uri; 21 import android.os.Bundle; 22 import android.os.Parcel; 23 import android.os.Parcelable; 24 import android.text.TextUtils; 25 26 import com.android.messaging.Factory; 27 import com.android.messaging.datamodel.BugleDatabaseOperations; 28 import com.android.messaging.datamodel.BugleNotifications; 29 import com.android.messaging.datamodel.DataModel; 30 import com.android.messaging.datamodel.DataModelException; 31 import com.android.messaging.datamodel.DatabaseHelper; 32 import com.android.messaging.datamodel.DatabaseHelper.MessageColumns; 33 import com.android.messaging.datamodel.DatabaseWrapper; 34 import com.android.messaging.datamodel.MessagingContentProvider; 35 import com.android.messaging.sms.MmsUtils; 36 import com.android.messaging.util.Assert; 37 import com.android.messaging.util.LogUtil; 38 import com.android.messaging.widget.WidgetConversationProvider; 39 40 import java.util.ArrayList; 41 import java.util.List; 42 43 /** 44 * Action used to delete a conversation. 45 */ 46 public class DeleteConversationAction extends Action implements Parcelable { 47 private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG; 48 49 public static void deleteConversation(final String conversationId, final long cutoffTimestamp) { 50 final DeleteConversationAction action = new DeleteConversationAction(conversationId, 51 cutoffTimestamp); 52 action.start(); 53 } 54 55 private static final String KEY_CONVERSATION_ID = "conversation_id"; 56 private static final String KEY_CUTOFF_TIMESTAMP = "cutoff_timestamp"; 57 58 private DeleteConversationAction(final String conversationId, final long cutoffTimestamp) { 59 super(); 60 actionParameters.putString(KEY_CONVERSATION_ID, conversationId); 61 // TODO: Should we set cuttoff timestamp to prevent us deleting new messages? 62 actionParameters.putLong(KEY_CUTOFF_TIMESTAMP, cutoffTimestamp); 63 } 64 65 // Delete conversation from both the local DB and telephony in the background so sync cannot 66 // run concurrently and incorrectly try to recreate the conversation's messages locally. The 67 // telephony database can sometimes be quite slow to delete conversations, so we delete from 68 // the local DB first, notify the UI, and then delete from telephony. 69 @Override 70 protected Bundle doBackgroundWork() throws DataModelException { 71 final DatabaseWrapper db = DataModel.get().getDatabase(); 72 73 final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID); 74 final long cutoffTimestamp = actionParameters.getLong(KEY_CUTOFF_TIMESTAMP); 75 76 if (!TextUtils.isEmpty(conversationId)) { 77 // First find the thread id for this conversation. 78 final long threadId = BugleDatabaseOperations.getThreadId(db, conversationId); 79 80 if (BugleDatabaseOperations.deleteConversation(db, conversationId, cutoffTimestamp)) { 81 LogUtil.i(TAG, "DeleteConversationAction: Deleted local conversation " 82 + conversationId); 83 84 BugleActionToasts.onConversationDeleted(); 85 86 // Remove notifications if necessary 87 BugleNotifications.update(true /* silent */, null /* conversationId */, 88 BugleNotifications.UPDATE_MESSAGES); 89 90 // We have changed the conversation list 91 MessagingContentProvider.notifyConversationListChanged(); 92 93 // Notify the widget the conversation is deleted so it can go into its configure state. 94 WidgetConversationProvider.notifyConversationDeleted( 95 Factory.get().getApplicationContext(), 96 conversationId); 97 } else { 98 LogUtil.w(TAG, "DeleteConversationAction: Could not delete local conversation " 99 + conversationId); 100 return null; 101 } 102 103 // Now delete from telephony DB. MmsSmsProvider throws an exception if the thread id is 104 // less than 0. If it's greater than zero, it will delete all messages with that thread 105 // id, even if there's no corresponding row in the threads table. 106 if (threadId >= 0) { 107 final int count = MmsUtils.deleteThread(threadId, cutoffTimestamp); 108 if (count > 0) { 109 LogUtil.i(TAG, "DeleteConversationAction: Deleted telephony thread " 110 + threadId + " (cutoffTimestamp = " + cutoffTimestamp + ")"); 111 } else { 112 LogUtil.w(TAG, "DeleteConversationAction: Could not delete thread from " 113 + "telephony: conversationId = " + conversationId + ", thread id = " 114 + threadId); 115 } 116 } else { 117 LogUtil.w(TAG, "DeleteConversationAction: Local conversation " + conversationId 118 + " has an invalid telephony thread id; will delete messages individually"); 119 deleteConversationMessagesFromTelephony(); 120 } 121 } else { 122 LogUtil.e(TAG, "DeleteConversationAction: conversationId is empty"); 123 } 124 125 return null; 126 } 127 128 /** 129 * Deletes all the telephony messages for the local conversation being deleted. 130 * <p> 131 * This is a fallback used when the conversation is not associated with any telephony thread, 132 * or its thread id is invalid (e.g. negative). This is not common, but can happen sometimes 133 * (e.g. the Unknown Sender conversation). In the usual case of deleting a conversation, we 134 * don't need this because the telephony provider automatically deletes messages when a thread 135 * is deleted. 136 */ 137 private void deleteConversationMessagesFromTelephony() { 138 final DatabaseWrapper db = DataModel.get().getDatabase(); 139 final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID); 140 Assert.notNull(conversationId); 141 142 final List<Uri> messageUris = new ArrayList<>(); 143 Cursor cursor = null; 144 try { 145 cursor = db.query(DatabaseHelper.MESSAGES_TABLE, 146 new String[] { MessageColumns.SMS_MESSAGE_URI }, 147 MessageColumns.CONVERSATION_ID + "=?", 148 new String[] { conversationId }, 149 null, null, null); 150 while (cursor.moveToNext()) { 151 String messageUri = cursor.getString(0); 152 try { 153 messageUris.add(Uri.parse(messageUri)); 154 } catch (Exception e) { 155 LogUtil.e(TAG, "DeleteConversationAction: Could not parse message uri " 156 + messageUri); 157 } 158 } 159 } finally { 160 if (cursor != null) { 161 cursor.close(); 162 } 163 } 164 for (Uri messageUri : messageUris) { 165 int count = MmsUtils.deleteMessage(messageUri); 166 if (count > 0) { 167 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { 168 LogUtil.d(TAG, "DeleteConversationAction: Deleted telephony message " 169 + messageUri); 170 } 171 } else { 172 LogUtil.w(TAG, "DeleteConversationAction: Could not delete telephony message " 173 + messageUri); 174 } 175 } 176 } 177 178 @Override 179 protected Object executeAction() { 180 requestBackgroundWork(); 181 return null; 182 } 183 184 private DeleteConversationAction(final Parcel in) { 185 super(in); 186 } 187 188 public static final Parcelable.Creator<DeleteConversationAction> CREATOR 189 = new Parcelable.Creator<DeleteConversationAction>() { 190 @Override 191 public DeleteConversationAction createFromParcel(final Parcel in) { 192 return new DeleteConversationAction(in); 193 } 194 195 @Override 196 public DeleteConversationAction[] newArray(final int size) { 197 return new DeleteConversationAction[size]; 198 } 199 }; 200 201 @Override 202 public void writeToParcel(final Parcel parcel, final int flags) { 203 writeActionToParcel(parcel, flags); 204 } 205 } 206