1 /*
2  * Copyright (C) 2014 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.example.android.apis.os;
18 
19 import com.example.android.mmslib.ContentType;
20 import com.example.android.mmslib.InvalidHeaderValueException;
21 import com.example.android.mmslib.pdu.CharacterSets;
22 import com.example.android.mmslib.pdu.EncodedStringValue;
23 import com.example.android.mmslib.pdu.GenericPdu;
24 import com.example.android.mmslib.pdu.PduBody;
25 import com.example.android.mmslib.pdu.PduComposer;
26 import com.example.android.mmslib.pdu.PduHeaders;
27 import com.example.android.mmslib.pdu.PduParser;
28 import com.example.android.mmslib.pdu.PduPart;
29 import com.example.android.mmslib.pdu.RetrieveConf;
30 import com.example.android.mmslib.pdu.SendConf;
31 import com.example.android.mmslib.pdu.SendReq;
32 
33 import android.app.Activity;
34 import android.app.PendingIntent;
35 import android.app.PendingIntent.CanceledException;
36 import android.content.BroadcastReceiver;
37 import android.content.ComponentName;
38 import android.content.ContentResolver;
39 import android.content.Context;
40 import android.content.Intent;
41 import android.content.IntentFilter;
42 import android.content.pm.PackageManager;
43 import android.net.Uri;
44 import android.os.AsyncTask;
45 import android.os.Bundle;
46 import android.os.ParcelFileDescriptor;
47 import android.telephony.PhoneNumberUtils;
48 import android.telephony.SmsManager;
49 import android.telephony.TelephonyManager;
50 import android.text.TextUtils;
51 import android.util.Log;
52 import android.view.View;
53 import android.widget.Button;
54 import android.widget.CheckBox;
55 import android.widget.CompoundButton;
56 import android.widget.EditText;
57 import android.widget.TextView;
58 
59 import com.example.android.apis.R;
60 
61 import java.io.File;
62 import java.io.FileInputStream;
63 import java.io.FileOutputStream;
64 import java.io.FileNotFoundException;
65 import java.io.IOException;
66 import java.util.Random;
67 
68 public class MmsMessagingDemo extends Activity {
69     private static final String TAG = "MmsMessagingDemo";
70 
71     public static final String EXTRA_NOTIFICATION_URL = "notification_url";
72 
73     private static final String ACTION_MMS_SENT = "com.example.android.apis.os.MMS_SENT_ACTION";
74     private static final String ACTION_MMS_RECEIVED =
75             "com.example.android.apis.os.MMS_RECEIVED_ACTION";
76 
77     private EditText mRecipientsInput;
78     private EditText mSubjectInput;
79     private EditText mTextInput;
80     private TextView mSendStatusView;
81     private Button mSendButton;
82     private File mSendFile;
83     private File mDownloadFile;
84     private Random mRandom = new Random();
85 
86     private BroadcastReceiver mSentReceiver = new BroadcastReceiver() {
87         @Override
88         public void onReceive(Context context, Intent intent) {
89             handleSentResult(getResultCode(), intent);
90         }
91     };
92     private IntentFilter mSentFilter = new IntentFilter(ACTION_MMS_SENT);
93 
94     private BroadcastReceiver mReceivedReceiver = new BroadcastReceiver() {
95         @Override
96         public void onReceive(Context context, Intent intent) {
97             handleReceivedResult(context, getResultCode(), intent);
98         }
99     };
100     private IntentFilter mReceivedFilter = new IntentFilter(ACTION_MMS_RECEIVED);
101 
102     @Override
onNewIntent(Intent intent)103     protected void onNewIntent(Intent intent) {
104         super.onNewIntent(intent);
105         final String notificationIndUrl = intent.getStringExtra(EXTRA_NOTIFICATION_URL);
106         if (!TextUtils.isEmpty(notificationIndUrl)) {
107             downloadMessage(notificationIndUrl);
108         }
109     }
110 
111     @Override
onCreate(Bundle savedInstanceState)112     protected void onCreate(Bundle savedInstanceState) {
113         super.onCreate(savedInstanceState);
114         setContentView(R.layout.mms_demo);
115 
116         // Enable or disable the broadcast receiver depending on the checked
117         // state of the checkbox.
118         final CheckBox enableCheckBox = (CheckBox) findViewById(R.id.mms_enable_receiver);
119         final PackageManager pm = this.getPackageManager();
120         final ComponentName componentName = new ComponentName("com.example.android.apis",
121                 "com.example.android.apis.os.MmsWapPushReceiver");
122         enableCheckBox.setChecked(pm.getComponentEnabledSetting(componentName) ==
123                 PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
124         enableCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
125             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
126                 Log.d(TAG, (isChecked ? "Enabling" : "Disabling") + " MMS receiver");
127                 pm.setComponentEnabledSetting(componentName,
128                         isChecked ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
129                                 : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
130                         PackageManager.DONT_KILL_APP);
131             }
132         });
133 
134         mRecipientsInput = (EditText) findViewById(R.id.mms_recipients_input);
135         mSubjectInput = (EditText) findViewById(R.id.mms_subject_input);
136         mTextInput = (EditText) findViewById(R.id.mms_text_input);
137         mSendStatusView = (TextView) findViewById(R.id.mms_send_status);
138         mSendButton = (Button) findViewById(R.id.mms_send_button);
139         mSendButton.setOnClickListener(new View.OnClickListener() {
140             @Override
141             public void onClick(View v) {
142                 sendMessage(
143                         mRecipientsInput.getText().toString(),
144                         mSubjectInput.getText().toString(),
145                         mTextInput.getText().toString());
146             }
147         });
148         registerReceiver(mSentReceiver, mSentFilter);
149         registerReceiver(mReceivedReceiver, mReceivedFilter);
150         final Intent intent = getIntent();
151         final String notificationIndUrl = intent.getStringExtra(EXTRA_NOTIFICATION_URL);
152         if (!TextUtils.isEmpty(notificationIndUrl)) {
153             downloadMessage(notificationIndUrl);
154         }
155     }
156 
sendMessage(final String recipients, final String subject, final String text)157     private void sendMessage(final String recipients, final String subject, final String text) {
158         Log.d(TAG, "Sending");
159         mSendStatusView.setText(getResources().getString(R.string.mms_status_sending));
160         mSendButton.setEnabled(false);
161         final String fileName = "send." + String.valueOf(Math.abs(mRandom.nextLong())) + ".dat";
162         mSendFile = new File(getCacheDir(), fileName);
163 
164         // Making RPC call in non-UI thread
165         AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
166             @Override
167             public void run() {
168                 final byte[] pdu = buildPdu(MmsMessagingDemo.this, recipients, subject, text);
169                 Uri writerUri = (new Uri.Builder())
170                        .authority("com.example.android.apis.os.MmsFileProvider")
171                        .path(fileName)
172                        .scheme(ContentResolver.SCHEME_CONTENT)
173                        .build();
174                 final PendingIntent pendingIntent = PendingIntent.getBroadcast(
175                         MmsMessagingDemo.this, 0, new Intent(ACTION_MMS_SENT), 0);
176                 FileOutputStream writer = null;
177                 Uri contentUri = null;
178                 try {
179                     writer = new FileOutputStream(mSendFile);
180                     writer.write(pdu);
181                     contentUri = writerUri;
182                 } catch (final IOException e) {
183                     Log.e(TAG, "Error writing send file", e);
184                 } finally {
185                     if (writer != null) {
186                         try {
187                             writer.close();
188                         } catch (IOException e) {
189                         }
190                     }
191                 }
192 
193                 if (contentUri != null) {
194                     SmsManager.getDefault().sendMultimediaMessage(getApplicationContext(),
195                             contentUri, null/*locationUrl*/, null/*configOverrides*/,
196                             pendingIntent);
197                 } else {
198                     Log.e(TAG, "Error writing sending Mms");
199                     try {
200                         pendingIntent.send(SmsManager.MMS_ERROR_IO_ERROR);
201                     } catch (CanceledException ex) {
202                         Log.e(TAG, "Mms pending intent cancelled?", ex);
203                     }
204                 }
205             }
206         });
207     }
208 
downloadMessage(final String locationUrl)209     private void downloadMessage(final String locationUrl) {
210         Log.d(TAG, "Downloading " + locationUrl);
211         mSendStatusView.setText(getResources().getString(R.string.mms_status_downloading));
212         mSendButton.setEnabled(false);
213         mRecipientsInput.setText("");
214         mSubjectInput.setText("");
215         mTextInput.setText("");
216         final String fileName = "download." + String.valueOf(Math.abs(mRandom.nextLong())) + ".dat";
217         mDownloadFile = new File(getCacheDir(), fileName);
218         // Making RPC call in non-UI thread
219         AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
220             @Override
221             public void run() {
222                 Uri contentUri = (new Uri.Builder())
223                         .authority("com.example.android.apis.os.MmsFileProvider")
224                         .path(fileName)
225                         .scheme(ContentResolver.SCHEME_CONTENT)
226                         .build();
227                 final PendingIntent pendingIntent = PendingIntent.getBroadcast(
228                         MmsMessagingDemo.this, 0, new Intent(ACTION_MMS_RECEIVED), 0);
229                 SmsManager.getDefault().downloadMultimediaMessage(getApplicationContext(),
230                         locationUrl, contentUri, null/*configOverrides*/, pendingIntent);
231             }
232         });
233     }
234 
handleSentResult(int code, Intent intent)235     private void handleSentResult(int code, Intent intent) {
236         mSendFile.delete();
237         int status = R.string.mms_status_failed;
238         if (code == Activity.RESULT_OK) {
239             final byte[] response = intent.getByteArrayExtra(SmsManager.EXTRA_MMS_DATA);
240             if (response != null) {
241                 final GenericPdu pdu = new PduParser(
242                         response, PduParserUtil.shouldParseContentDisposition()).parse();
243                 if (pdu instanceof SendConf) {
244                     final SendConf sendConf = (SendConf) pdu;
245                     if (sendConf.getResponseStatus() == PduHeaders.RESPONSE_STATUS_OK) {
246                         status = R.string.mms_status_sent;
247                     } else {
248                         Log.e(TAG, "MMS sent, error=" + sendConf.getResponseStatus());
249                     }
250                 } else {
251                     Log.e(TAG, "MMS sent, invalid response");
252                 }
253             } else {
254                 Log.e(TAG, "MMS sent, empty response");
255             }
256         } else {
257             Log.e(TAG, "MMS not sent, error=" + code);
258         }
259 
260         mSendFile = null;
261         mSendStatusView.setText(status);
262         mSendButton.setEnabled(true);
263     }
264 
265     @Override
onDestroy()266     protected void onDestroy() {
267         super.onDestroy();
268         if (mSentReceiver != null) {
269             unregisterReceiver(mSentReceiver);
270         }
271         if (mReceivedReceiver != null) {
272             unregisterReceiver(mReceivedReceiver);
273         }
274     }
275 
handleReceivedResult(Context context, int code, Intent intent)276     private void handleReceivedResult(Context context, int code, Intent intent) {
277         int status = R.string.mms_status_failed;
278         if (code == Activity.RESULT_OK) {
279             try {
280                 final int nBytes = (int) mDownloadFile.length();
281                 FileInputStream reader = new FileInputStream(mDownloadFile);
282                 final byte[] response = new byte[nBytes];
283                 final int read = reader.read(response, 0, nBytes);
284                 if (read == nBytes) {
285                     final GenericPdu pdu = new PduParser(
286                             response, PduParserUtil.shouldParseContentDisposition()).parse();
287                     if (pdu instanceof RetrieveConf) {
288                         final RetrieveConf retrieveConf = (RetrieveConf) pdu;
289                         mRecipientsInput.setText(getRecipients(context, retrieveConf));
290                         mSubjectInput.setText(getSubject(retrieveConf));
291                         mTextInput.setText(getMessageText(retrieveConf));
292                         status = R.string.mms_status_downloaded;
293                     } else {
294                         Log.e(TAG, "MMS received, invalid response");
295                     }
296                 } else {
297                     Log.e(TAG, "MMS received, empty response");
298                 }
299             } catch (FileNotFoundException e) {
300                 Log.e(TAG, "MMS received, file not found exception", e);
301             } catch (IOException e) {
302                 Log.e(TAG, "MMS received, io exception", e);
303             } finally {
304                 mDownloadFile.delete();
305             }
306         } else {
307             Log.e(TAG, "MMS not received, error=" + code);
308         }
309         mDownloadFile = null;
310         mSendStatusView.setText(status);
311         mSendButton.setEnabled(true);
312     }
313 
314     public static final long DEFAULT_EXPIRY_TIME = 7 * 24 * 60 * 60;
315     public static final int DEFAULT_PRIORITY = PduHeaders.PRIORITY_NORMAL;
316 
317     private static final String TEXT_PART_FILENAME = "text_0.txt";
318     private static final String sSmilText =
319             "<smil>" +
320                 "<head>" +
321                     "<layout>" +
322                         "<root-layout/>" +
323                         "<region height=\"100%%\" id=\"Text\" left=\"0%%\" top=\"0%%\" width=\"100%%\"/>" +
324                     "</layout>" +
325                 "</head>" +
326                 "<body>" +
327                     "<par dur=\"8000ms\">" +
328                         "<text src=\"%s\" region=\"Text\"/>" +
329                     "</par>" +
330                 "</body>" +
331             "</smil>";
332 
buildPdu(Context context, String recipients, String subject, String text)333     private static byte[] buildPdu(Context context, String recipients, String subject,
334             String text) {
335         final SendReq req = new SendReq();
336         // From, per spec
337         final String lineNumber = getSimNumber(context);
338         if (!TextUtils.isEmpty(lineNumber)) {
339             req.setFrom(new EncodedStringValue(lineNumber));
340         }
341         // To
342         EncodedStringValue[] encodedNumbers =
343                 EncodedStringValue.encodeStrings(recipients.split(" "));
344         if (encodedNumbers != null) {
345             req.setTo(encodedNumbers);
346         }
347         // Subject
348         if (!TextUtils.isEmpty(subject)) {
349             req.setSubject(new EncodedStringValue(subject));
350         }
351         // Date
352         req.setDate(System.currentTimeMillis() / 1000);
353         // Body
354         PduBody body = new PduBody();
355         // Add text part. Always add a smil part for compatibility, without it there
356         // may be issues on some carriers/client apps
357         final int size = addTextPart(body, text, true/* add text smil */);
358         req.setBody(body);
359         // Message size
360         req.setMessageSize(size);
361         // Message class
362         req.setMessageClass(PduHeaders.MESSAGE_CLASS_PERSONAL_STR.getBytes());
363         // Expiry
364         req.setExpiry(DEFAULT_EXPIRY_TIME);
365         try {
366             // Priority
367             req.setPriority(DEFAULT_PRIORITY);
368             // Delivery report
369             req.setDeliveryReport(PduHeaders.VALUE_NO);
370             // Read report
371             req.setReadReport(PduHeaders.VALUE_NO);
372         } catch (InvalidHeaderValueException e) {}
373 
374         return new PduComposer(context, req).make();
375     }
376 
addTextPart(PduBody pb, String message, boolean addTextSmil)377     private static int addTextPart(PduBody pb, String message, boolean addTextSmil) {
378         final PduPart part = new PduPart();
379         // Set Charset if it's a text media.
380         part.setCharset(CharacterSets.UTF_8);
381         // Set Content-Type.
382         part.setContentType(ContentType.TEXT_PLAIN.getBytes());
383         // Set Content-Location.
384         part.setContentLocation(TEXT_PART_FILENAME.getBytes());
385         int index = TEXT_PART_FILENAME.lastIndexOf(".");
386         String contentId = (index == -1) ? TEXT_PART_FILENAME
387                 : TEXT_PART_FILENAME.substring(0, index);
388         part.setContentId(contentId.getBytes());
389         part.setData(message.getBytes());
390         pb.addPart(part);
391         if (addTextSmil) {
392             final String smil = String.format(sSmilText, TEXT_PART_FILENAME);
393             addSmilPart(pb, smil);
394         }
395         return part.getData().length;
396     }
397 
addSmilPart(PduBody pb, String smil)398     private static void addSmilPart(PduBody pb, String smil) {
399         final PduPart smilPart = new PduPart();
400         smilPart.setContentId("smil".getBytes());
401         smilPart.setContentLocation("smil.xml".getBytes());
402         smilPart.setContentType(ContentType.APP_SMIL.getBytes());
403         smilPart.setData(smil.getBytes());
404         pb.addPart(0, smilPart);
405     }
406 
getRecipients(Context context, RetrieveConf retrieveConf)407     private static String getRecipients(Context context, RetrieveConf retrieveConf) {
408         final String self = getSimNumber(context);
409         final StringBuilder sb = new StringBuilder();
410         if (retrieveConf.getFrom() != null) {
411             sb.append(retrieveConf.getFrom().getString());
412         }
413         if (retrieveConf.getTo() != null) {
414             for (EncodedStringValue to : retrieveConf.getTo()) {
415                 final String number = to.getString();
416                 if (!PhoneNumberUtils.compare(number, self)) {
417                     sb.append(" ").append(to.getString());
418                 }
419             }
420         }
421         if (retrieveConf.getCc() != null) {
422             for (EncodedStringValue cc : retrieveConf.getCc()) {
423                 final String number = cc.getString();
424                 if (!PhoneNumberUtils.compare(number, self)) {
425                     sb.append(" ").append(cc.getString());
426                 }
427             }
428         }
429         return sb.toString();
430     }
431 
getSubject(RetrieveConf retrieveConf)432     private static String getSubject(RetrieveConf retrieveConf) {
433         final EncodedStringValue subject = retrieveConf.getSubject();
434         return subject != null ? subject.getString() : "";
435     }
436 
getMessageText(RetrieveConf retrieveConf)437     private static String getMessageText(RetrieveConf retrieveConf) {
438         final StringBuilder sb = new StringBuilder();
439         final PduBody body = retrieveConf.getBody();
440         if (body != null) {
441             for (int i = 0; i < body.getPartsNum(); i++) {
442                 final PduPart part = body.getPart(i);
443                 if (part != null
444                         && part.getContentType() != null
445                         && ContentType.isTextType(new String(part.getContentType()))) {
446                     sb.append(new String(part.getData()));
447                 }
448             }
449         }
450         return sb.toString();
451     }
452 
getSimNumber(Context context)453     private static String getSimNumber(Context context) {
454         final TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(
455                 Context.TELEPHONY_SERVICE);
456         return telephonyManager.getLine1Number();
457     }
458 }
459