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
17package com.android.messaging.ui.conversation;
18
19import android.app.AlertDialog;
20import android.content.Context;
21import android.content.res.Resources;
22import android.net.Uri;
23import android.text.TextUtils;
24import android.text.format.Formatter;
25
26import com.android.messaging.Factory;
27import com.android.messaging.R;
28import com.android.messaging.datamodel.BugleDatabaseOperations;
29import com.android.messaging.datamodel.DataModel;
30import com.android.messaging.datamodel.data.ConversationMessageData;
31import com.android.messaging.datamodel.data.ConversationParticipantsData;
32import com.android.messaging.datamodel.data.ParticipantData;
33import com.android.messaging.mmslib.pdu.PduHeaders;
34import com.android.messaging.sms.DatabaseMessages.MmsMessage;
35import com.android.messaging.sms.MmsUtils;
36import com.android.messaging.util.Assert;
37import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
38import com.android.messaging.util.Dates;
39import com.android.messaging.util.DebugUtils;
40import com.android.messaging.util.OsUtil;
41import com.android.messaging.util.PhoneUtils;
42import com.android.messaging.util.SafeAsyncTask;
43
44import java.util.List;
45
46public 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