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.google.android.mms.ContentType; 20 import com.google.android.mms.InvalidHeaderValueException; 21 import com.google.android.mms.pdu.CharacterSets; 22 import com.google.android.mms.pdu.EncodedStringValue; 23 import com.google.android.mms.pdu.GenericPdu; 24 import com.google.android.mms.pdu.PduBody; 25 import com.google.android.mms.pdu.PduComposer; 26 import com.google.android.mms.pdu.PduHeaders; 27 import com.google.android.mms.pdu.PduParser; 28 import com.google.android.mms.pdu.PduPart; 29 import com.google.android.mms.pdu.RetrieveConf; 30 import com.google.android.mms.pdu.SendConf; 31 import com.google.android.mms.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