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.ui.conversation; 18 19 import android.app.AlertDialog; 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.net.Uri; 23 import android.text.TextUtils; 24 import android.text.format.Formatter; 25 26 import com.android.messaging.Factory; 27 import com.android.messaging.R; 28 import com.android.messaging.datamodel.BugleDatabaseOperations; 29 import com.android.messaging.datamodel.DataModel; 30 import com.android.messaging.datamodel.data.ConversationMessageData; 31 import com.android.messaging.datamodel.data.ConversationParticipantsData; 32 import com.android.messaging.datamodel.data.ParticipantData; 33 import com.android.messaging.mmslib.pdu.PduHeaders; 34 import com.android.messaging.sms.DatabaseMessages.MmsMessage; 35 import com.android.messaging.sms.MmsUtils; 36 import com.android.messaging.util.Assert; 37 import com.android.messaging.util.Assert.DoesNotRunOnMainThread; 38 import com.android.messaging.util.Dates; 39 import com.android.messaging.util.DebugUtils; 40 import com.android.messaging.util.OsUtil; 41 import com.android.messaging.util.PhoneUtils; 42 import com.android.messaging.util.SafeAsyncTask; 43 44 import java.util.List; 45 46 public class MessageDetailsDialog { 47 private static final String RECIPIENT_SEPARATOR = ", "; 48 49 // All methods are static, no creating this class 50 private MessageDetailsDialog() { 51 } 52 53 public static void show(final Context context, final ConversationMessageData data, 54 final ConversationParticipantsData participants, final ParticipantData self) { 55 if (DebugUtils.isDebugEnabled()) { 56 new SafeAsyncTask<Void, Void, String>() { 57 @Override 58 protected String doInBackgroundTimed(Void... params) { 59 return getMessageDetails(context, data, participants, self); 60 } 61 62 @Override 63 protected void onPostExecute(String messageDetails) { 64 showDialog(context, messageDetails); 65 } 66 }.executeOnThreadPool(null, null, null); 67 } else { 68 String messageDetails = getMessageDetails(context, data, participants, self); 69 showDialog(context, messageDetails); 70 } 71 } 72 73 private static String getMessageDetails(final Context context, 74 final ConversationMessageData data, 75 final ConversationParticipantsData participants, final ParticipantData self) { 76 String messageDetails = null; 77 if (data.getIsSms()) { 78 messageDetails = getSmsMessageDetails(data, participants, self); 79 } else { 80 // TODO: Handle SMS_TYPE_MMS_PUSH_NOTIFICATION type differently? 81 messageDetails = getMmsMessageDetails(context, data, participants, self); 82 } 83 84 return messageDetails; 85 } 86 87 private static void showDialog(final Context context, String messageDetails) { 88 if (!TextUtils.isEmpty(messageDetails)) { 89 new AlertDialog.Builder(context) 90 .setTitle(R.string.message_details_title) 91 .setMessage(messageDetails) 92 .setCancelable(true) 93 .show(); 94 } 95 } 96 97 /** 98 * Return a string, separated by newlines, that contains a number of labels and values 99 * for this sms message. The string will be displayed in a modal dialog. 100 * @return string list of various message properties 101 */ 102 private static String getSmsMessageDetails(final ConversationMessageData data, 103 final ConversationParticipantsData participants, final ParticipantData self) { 104 final Resources res = Factory.get().getApplicationContext().getResources(); 105 final StringBuilder details = new StringBuilder(); 106 107 // Type: Text message 108 details.append(res.getString(R.string.message_type_label)); 109 details.append(res.getString(R.string.text_message)); 110 111 // From: +1425xxxxxxx 112 // or To: +1425xxxxxxx 113 final String rawSender = data.getSenderNormalizedDestination(); 114 if (!TextUtils.isEmpty(rawSender)) { 115 details.append('\n'); 116 details.append(res.getString(R.string.from_label)); 117 details.append(rawSender); 118 } 119 final String rawRecipients = getRecipientParticipantString(participants, 120 data.getParticipantId(), data.getIsIncoming(), data.getSelfParticipantId()); 121 if (!TextUtils.isEmpty(rawRecipients)) { 122 details.append('\n'); 123 details.append(res.getString(R.string.to_address_label)); 124 details.append(rawRecipients); 125 } 126 127 // Sent: Mon 11:42AM 128 if (data.getIsIncoming()) { 129 if (data.getSentTimeStamp() != MmsUtils.INVALID_TIMESTAMP) { 130 details.append('\n'); 131 details.append(res.getString(R.string.sent_label)); 132 details.append( 133 Dates.getMessageDetailsTimeString(data.getSentTimeStamp()).toString()); 134 } 135 } 136 137 // Sent: Mon 11:43AM 138 // or Received: Mon 11:43AM 139 appendSentOrReceivedTimestamp(res, details, data); 140 141 appendSimInfo(res, self, details); 142 143 if (DebugUtils.isDebugEnabled()) { 144 appendDebugInfo(details, data); 145 } 146 147 return details.toString(); 148 } 149 150 /** 151 * Return a string, separated by newlines, that contains a number of labels and values 152 * for this mms message. The string will be displayed in a modal dialog. 153 * @return string list of various message properties 154 */ 155 private static String getMmsMessageDetails(Context context, final ConversationMessageData data, 156 final ConversationParticipantsData participants, final ParticipantData self) { 157 final Resources res = Factory.get().getApplicationContext().getResources(); 158 // TODO: when we support non-auto-download of mms messages, we'll have to handle 159 // the case when the message is a PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND and display 160 // something different. See the Messaging app's MessageUtils.getNotificationIndDetails() 161 162 final StringBuilder details = new StringBuilder(); 163 164 // Type: Multimedia message. 165 details.append(res.getString(R.string.message_type_label)); 166 details.append(res.getString(R.string.multimedia_message)); 167 168 // From: +1425xxxxxxx 169 final String rawSender = data.getSenderNormalizedDestination(); 170 details.append('\n'); 171 details.append(res.getString(R.string.from_label)); 172 details.append(!TextUtils.isEmpty(rawSender) ? rawSender : 173 res.getString(R.string.hidden_sender_address)); 174 175 // To: +1425xxxxxxx 176 final String rawRecipients = getRecipientParticipantString(participants, 177 data.getParticipantId(), data.getIsIncoming(), data.getSelfParticipantId()); 178 if (!TextUtils.isEmpty(rawRecipients)) { 179 details.append('\n'); 180 details.append(res.getString(R.string.to_address_label)); 181 details.append(rawRecipients); 182 } 183 184 // Sent: Tue 3:05PM 185 // or Received: Tue 3:05PM 186 appendSentOrReceivedTimestamp(res, details, data); 187 188 // Subject: You're awesome 189 details.append('\n'); 190 details.append(res.getString(R.string.subject_label)); 191 if (!TextUtils.isEmpty(MmsUtils.cleanseMmsSubject(res, data.getMmsSubject()))) { 192 details.append(data.getMmsSubject()); 193 } 194 195 // Priority: High/Normal/Low 196 details.append('\n'); 197 details.append(res.getString(R.string.priority_label)); 198 details.append(getPriorityDescription(res, data.getSmsPriority())); 199 200 // Message size: 30 KB 201 if (data.getSmsMessageSize() > 0) { 202 details.append('\n'); 203 details.append(res.getString(R.string.message_size_label)); 204 details.append(Formatter.formatFileSize(context, data.getSmsMessageSize())); 205 } 206 207 appendSimInfo(res, self, details); 208 209 if (DebugUtils.isDebugEnabled()) { 210 appendDebugInfo(details, data); 211 } 212 213 return details.toString(); 214 } 215 216 private static void appendSentOrReceivedTimestamp(Resources res, StringBuilder details, 217 ConversationMessageData data) { 218 int labelId = -1; 219 if (data.getIsIncoming()) { 220 labelId = R.string.received_label; 221 } else if (data.getIsSendComplete()) { 222 labelId = R.string.sent_label; 223 } 224 if (labelId >= 0) { 225 details.append('\n'); 226 details.append(res.getString(labelId)); 227 details.append( 228 Dates.getMessageDetailsTimeString(data.getReceivedTimeStamp()).toString()); 229 } 230 } 231 232 @DoesNotRunOnMainThread 233 private static void appendDebugInfo(StringBuilder details, ConversationMessageData data) { 234 // We grab the thread id from the database, so this needs to run in the background 235 Assert.isNotMainThread(); 236 details.append("\n\n"); 237 details.append("DEBUG"); 238 239 details.append('\n'); 240 details.append("Message id: "); 241 details.append(data.getMessageId()); 242 243 final String telephonyUri = data.getSmsMessageUri(); 244 details.append('\n'); 245 details.append("Telephony uri: "); 246 details.append(telephonyUri); 247 248 final String conversationId = data.getConversationId(); 249 250 if (conversationId == null) { 251 return; 252 } 253 254 details.append('\n'); 255 details.append("Conversation id: "); 256 details.append(conversationId); 257 258 final long threadId = BugleDatabaseOperations.getThreadId(DataModel.get().getDatabase(), 259 conversationId); 260 261 details.append('\n'); 262 details.append("Conversation telephony thread id: "); 263 details.append(threadId); 264 265 MmsMessage mms = null; 266 267 if (data.getIsMms()) { 268 if (telephonyUri == null) { 269 return; 270 } 271 mms = MmsUtils.loadMms(Uri.parse(telephonyUri)); 272 if (mms == null) { 273 return; 274 } 275 276 // We log the thread id again to check that they are internally consistent 277 final long mmsThreadId = mms.mThreadId; 278 details.append('\n'); 279 details.append("Telephony thread id: "); 280 details.append(mmsThreadId); 281 282 // Log the MMS content location 283 final String mmsContentLocation = mms.mContentLocation; 284 details.append('\n'); 285 details.append("Content location URL: "); 286 details.append(mmsContentLocation); 287 } 288 289 final String recipientsString = MmsUtils.getRawRecipientIdsForThread(threadId); 290 if (recipientsString != null) { 291 details.append('\n'); 292 details.append("Thread recipient ids: "); 293 details.append(recipientsString); 294 } 295 296 final List<String> recipients = MmsUtils.getRecipientsByThread(threadId); 297 if (recipients != null) { 298 details.append('\n'); 299 details.append("Thread recipients: "); 300 details.append(recipients.toString()); 301 302 if (mms != null) { 303 final String from = MmsUtils.getMmsSender(recipients, mms.getUri()); 304 details.append('\n'); 305 details.append("Sender: "); 306 details.append(from); 307 } 308 } 309 } 310 311 private static String getRecipientParticipantString( 312 final ConversationParticipantsData participants, final String senderId, 313 final boolean addSelf, final String selfId) { 314 final StringBuilder recipients = new StringBuilder(); 315 for (final ParticipantData participant : participants) { 316 if (TextUtils.equals(participant.getId(), senderId)) { 317 // Don't add sender 318 continue; 319 } 320 if (participant.isSelf() && 321 (!participant.getId().equals(selfId) || !addSelf)) { 322 // For self participants, don't add the one that's not relevant to this message 323 // or if we are asked not to add self 324 continue; 325 } 326 final String phoneNumber = participant.getNormalizedDestination(); 327 // Don't add empty number. This should not happen. But if that happens 328 // we should not add it. 329 if (!TextUtils.isEmpty(phoneNumber)) { 330 if (recipients.length() > 0) { 331 recipients.append(RECIPIENT_SEPARATOR); 332 } 333 recipients.append(phoneNumber); 334 } 335 } 336 return recipients.toString(); 337 } 338 339 /** 340 * Convert the numeric mms priority into a human-readable string 341 * @param res 342 * @param priorityValue coded PduHeader priority 343 * @return string representation of the priority 344 */ 345 private static String getPriorityDescription(final Resources res, final int priorityValue) { 346 switch(priorityValue) { 347 case PduHeaders.PRIORITY_HIGH: 348 return res.getString(R.string.priority_high); 349 case PduHeaders.PRIORITY_LOW: 350 return res.getString(R.string.priority_low); 351 case PduHeaders.PRIORITY_NORMAL: 352 default: 353 return res.getString(R.string.priority_normal); 354 } 355 } 356 357 private static void appendSimInfo(final Resources res, 358 final ParticipantData self, final StringBuilder outString) { 359 if (!OsUtil.isAtLeastL_MR1() 360 || self == null 361 || PhoneUtils.getDefault().getActiveSubscriptionCount() < 2) { 362 return; 363 } 364 // The appended SIM info would look like: 365 // SIM: SUB 01 366 // or SIM: SIM 1 367 // or SIM: Unknown 368 Assert.isTrue(self.isSelf()); 369 outString.append('\n'); 370 outString.append(res.getString(R.string.sim_label)); 371 if (self.isActiveSubscription() && !self.isDefaultSelf()) { 372 final String subscriptionName = self.getSubscriptionName(); 373 if (TextUtils.isEmpty(subscriptionName)) { 374 outString.append(res.getString(R.string.sim_slot_identifier, 375 self.getDisplaySlotId())); 376 } else { 377 outString.append(subscriptionName); 378 } 379 } 380 } 381 } 382