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
MessageDetailsDialog()50     private MessageDetailsDialog() {
51     }
52 
show(final Context context, final ConversationMessageData data, final ConversationParticipantsData participants, final ParticipantData self)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 
getMessageDetails(final Context context, final ConversationMessageData data, final ConversationParticipantsData participants, final ParticipantData self)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 
showDialog(final Context context, String messageDetails)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      */
getSmsMessageDetails(final ConversationMessageData data, final ConversationParticipantsData participants, final ParticipantData self)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      */
getMmsMessageDetails(Context context, final ConversationMessageData data, final ConversationParticipantsData participants, final ParticipantData self)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 
appendSentOrReceivedTimestamp(Resources res, StringBuilder details, ConversationMessageData data)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
appendDebugInfo(StringBuilder details, ConversationMessageData data)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 
getRecipientParticipantString( final ConversationParticipantsData participants, final String senderId, final boolean addSelf, final String selfId)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      */
getPriorityDescription(final Resources res, final int priorityValue)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 
appendSimInfo(final Resources res, final ParticipantData self, final StringBuilder outString)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