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 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 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 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 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 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(response).parse(); 242 if (pdu instanceof SendConf) { 243 final SendConf sendConf = (SendConf) pdu; 244 if (sendConf.getResponseStatus() == PduHeaders.RESPONSE_STATUS_OK) { 245 status = R.string.mms_status_sent; 246 } else { 247 Log.e(TAG, "MMS sent, error=" + sendConf.getResponseStatus()); 248 } 249 } else { 250 Log.e(TAG, "MMS sent, invalid response"); 251 } 252 } else { 253 Log.e(TAG, "MMS sent, empty response"); 254 } 255 } else { 256 Log.e(TAG, "MMS not sent, error=" + code); 257 } 258 259 mSendFile = null; 260 mSendStatusView.setText(status); 261 mSendButton.setEnabled(true); 262 } 263 264 @Override 265 protected void onDestroy() { 266 super.onDestroy(); 267 if (mSentReceiver != null) { 268 unregisterReceiver(mSentReceiver); 269 } 270 if (mReceivedReceiver != null) { 271 unregisterReceiver(mReceivedReceiver); 272 } 273 } 274 275 private void handleReceivedResult(Context context, int code, Intent intent) { 276 int status = R.string.mms_status_failed; 277 if (code == Activity.RESULT_OK) { 278 try { 279 final int nBytes = (int) mDownloadFile.length(); 280 FileInputStream reader = new FileInputStream(mDownloadFile); 281 final byte[] response = new byte[nBytes]; 282 final int read = reader.read(response, 0, nBytes); 283 if (read == nBytes) { 284 final GenericPdu pdu = new PduParser(response).parse(); 285 if (pdu instanceof RetrieveConf) { 286 final RetrieveConf retrieveConf = (RetrieveConf) pdu; 287 mRecipientsInput.setText(getRecipients(context, retrieveConf)); 288 mSubjectInput.setText(getSubject(retrieveConf)); 289 mTextInput.setText(getMessageText(retrieveConf)); 290 status = R.string.mms_status_downloaded; 291 } else { 292 Log.e(TAG, "MMS received, invalid response"); 293 } 294 } else { 295 Log.e(TAG, "MMS received, empty response"); 296 } 297 } catch (FileNotFoundException e) { 298 Log.e(TAG, "MMS received, file not found exception", e); 299 } catch (IOException e) { 300 Log.e(TAG, "MMS received, io exception", e); 301 } finally { 302 mDownloadFile.delete(); 303 } 304 } else { 305 Log.e(TAG, "MMS not received, error=" + code); 306 } 307 mDownloadFile = null; 308 mSendStatusView.setText(status); 309 mSendButton.setEnabled(true); 310 } 311 312 public static final long DEFAULT_EXPIRY_TIME = 7 * 24 * 60 * 60; 313 public static final int DEFAULT_PRIORITY = PduHeaders.PRIORITY_NORMAL; 314 315 private static final String TEXT_PART_FILENAME = "text_0.txt"; 316 private static final String sSmilText = 317 "<smil>" + 318 "<head>" + 319 "<layout>" + 320 "<root-layout/>" + 321 "<region height=\"100%%\" id=\"Text\" left=\"0%%\" top=\"0%%\" width=\"100%%\"/>" + 322 "</layout>" + 323 "</head>" + 324 "<body>" + 325 "<par dur=\"8000ms\">" + 326 "<text src=\"%s\" region=\"Text\"/>" + 327 "</par>" + 328 "</body>" + 329 "</smil>"; 330 331 private static byte[] buildPdu(Context context, String recipients, String subject, 332 String text) { 333 final SendReq req = new SendReq(); 334 // From, per spec 335 final String lineNumber = getSimNumber(context); 336 if (!TextUtils.isEmpty(lineNumber)) { 337 req.setFrom(new EncodedStringValue(lineNumber)); 338 } 339 // To 340 EncodedStringValue[] encodedNumbers = 341 EncodedStringValue.encodeStrings(recipients.split(" ")); 342 if (encodedNumbers != null) { 343 req.setTo(encodedNumbers); 344 } 345 // Subject 346 if (!TextUtils.isEmpty(subject)) { 347 req.setSubject(new EncodedStringValue(subject)); 348 } 349 // Date 350 req.setDate(System.currentTimeMillis() / 1000); 351 // Body 352 PduBody body = new PduBody(); 353 // Add text part. Always add a smil part for compatibility, without it there 354 // may be issues on some carriers/client apps 355 final int size = addTextPart(body, text, true/* add text smil */); 356 req.setBody(body); 357 // Message size 358 req.setMessageSize(size); 359 // Message class 360 req.setMessageClass(PduHeaders.MESSAGE_CLASS_PERSONAL_STR.getBytes()); 361 // Expiry 362 req.setExpiry(DEFAULT_EXPIRY_TIME); 363 try { 364 // Priority 365 req.setPriority(DEFAULT_PRIORITY); 366 // Delivery report 367 req.setDeliveryReport(PduHeaders.VALUE_NO); 368 // Read report 369 req.setReadReport(PduHeaders.VALUE_NO); 370 } catch (InvalidHeaderValueException e) {} 371 372 return new PduComposer(context, req).make(); 373 } 374 375 private static int addTextPart(PduBody pb, String message, boolean addTextSmil) { 376 final PduPart part = new PduPart(); 377 // Set Charset if it's a text media. 378 part.setCharset(CharacterSets.UTF_8); 379 // Set Content-Type. 380 part.setContentType(ContentType.TEXT_PLAIN.getBytes()); 381 // Set Content-Location. 382 part.setContentLocation(TEXT_PART_FILENAME.getBytes()); 383 int index = TEXT_PART_FILENAME.lastIndexOf("."); 384 String contentId = (index == -1) ? TEXT_PART_FILENAME 385 : TEXT_PART_FILENAME.substring(0, index); 386 part.setContentId(contentId.getBytes()); 387 part.setData(message.getBytes()); 388 pb.addPart(part); 389 if (addTextSmil) { 390 final String smil = String.format(sSmilText, TEXT_PART_FILENAME); 391 addSmilPart(pb, smil); 392 } 393 return part.getData().length; 394 } 395 396 private static void addSmilPart(PduBody pb, String smil) { 397 final PduPart smilPart = new PduPart(); 398 smilPart.setContentId("smil".getBytes()); 399 smilPart.setContentLocation("smil.xml".getBytes()); 400 smilPart.setContentType(ContentType.APP_SMIL.getBytes()); 401 smilPart.setData(smil.getBytes()); 402 pb.addPart(0, smilPart); 403 } 404 405 private static String getRecipients(Context context, RetrieveConf retrieveConf) { 406 final String self = getSimNumber(context); 407 final StringBuilder sb = new StringBuilder(); 408 if (retrieveConf.getFrom() != null) { 409 sb.append(retrieveConf.getFrom().getString()); 410 } 411 if (retrieveConf.getTo() != null) { 412 for (EncodedStringValue to : retrieveConf.getTo()) { 413 final String number = to.getString(); 414 if (!PhoneNumberUtils.compare(number, self)) { 415 sb.append(" ").append(to.getString()); 416 } 417 } 418 } 419 if (retrieveConf.getCc() != null) { 420 for (EncodedStringValue cc : retrieveConf.getCc()) { 421 final String number = cc.getString(); 422 if (!PhoneNumberUtils.compare(number, self)) { 423 sb.append(" ").append(cc.getString()); 424 } 425 } 426 } 427 return sb.toString(); 428 } 429 430 private static String getSubject(RetrieveConf retrieveConf) { 431 final EncodedStringValue subject = retrieveConf.getSubject(); 432 return subject != null ? subject.getString() : ""; 433 } 434 435 private static String getMessageText(RetrieveConf retrieveConf) { 436 final StringBuilder sb = new StringBuilder(); 437 final PduBody body = retrieveConf.getBody(); 438 if (body != null) { 439 for (int i = 0; i < body.getPartsNum(); i++) { 440 final PduPart part = body.getPart(i); 441 if (part != null 442 && part.getContentType() != null 443 && ContentType.isTextType(new String(part.getContentType()))) { 444 sb.append(new String(part.getData())); 445 } 446 } 447 } 448 return sb.toString(); 449 } 450 451 private static String getSimNumber(Context context) { 452 final TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService( 453 Context.TELEPHONY_SERVICE); 454 return telephonyManager.getLine1Number(); 455 } 456 } 457