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 package com.android.phone.vvm.omtp.imap; 17 18 import android.content.Context; 19 import android.content.SharedPreferences; 20 import android.net.ConnectivityManager; 21 import android.net.Network; 22 import android.net.NetworkInfo; 23 import android.preference.PreferenceManager; 24 import android.provider.VoicemailContract; 25 import android.provider.VoicemailContract.Status; 26 import android.telecom.PhoneAccountHandle; 27 import android.telecom.Voicemail; 28 import android.telephony.TelephonyManager; 29 import android.util.Base64; 30 import android.util.Log; 31 32 import com.android.phone.PhoneUtils; 33 import com.android.phone.VoicemailUtils; 34 import com.android.phone.common.mail.Address; 35 import com.android.phone.common.mail.Body; 36 import com.android.phone.common.mail.BodyPart; 37 import com.android.phone.common.mail.FetchProfile; 38 import com.android.phone.common.mail.Flag; 39 import com.android.phone.common.mail.Message; 40 import com.android.phone.common.mail.MessagingException; 41 import com.android.phone.common.mail.Multipart; 42 import com.android.phone.common.mail.TempDirectory; 43 import com.android.phone.common.mail.internet.MimeMessage; 44 import com.android.phone.common.mail.store.ImapFolder; 45 import com.android.phone.common.mail.store.ImapStore; 46 import com.android.phone.common.mail.store.imap.ImapConstants; 47 import com.android.phone.common.mail.utils.LogUtils; 48 import com.android.phone.settings.VisualVoicemailSettingsUtil; 49 import com.android.phone.vvm.omtp.OmtpConstants; 50 import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper; 51 import com.android.phone.vvm.omtp.fetch.VoicemailFetchedCallback; 52 import com.android.phone.vvm.omtp.sync.OmtpVvmSyncService.TranscriptionFetchedCallback; 53 54 import libcore.io.IoUtils; 55 56 import java.io.BufferedOutputStream; 57 import java.io.ByteArrayOutputStream; 58 import java.io.IOException; 59 import java.util.ArrayList; 60 import java.util.Arrays; 61 import java.util.List; 62 63 /** 64 * A helper interface to abstract commands sent across IMAP interface for a given account. 65 */ 66 public class ImapHelper { 67 private final String TAG = "ImapHelper"; 68 69 private ImapFolder mFolder; 70 private ImapStore mImapStore; 71 72 private final Context mContext; 73 private final PhoneAccountHandle mPhoneAccount; 74 private final Network mNetwork; 75 76 SharedPreferences mPrefs; 77 private static final String PREF_KEY_QUOTA_OCCUPIED = "quota_occupied_"; 78 private static final String PREF_KEY_QUOTA_TOTAL = "quota_total_"; 79 80 private int mQuotaOccupied; 81 private int mQuotaTotal; 82 ImapHelper(Context context, PhoneAccountHandle phoneAccount, Network network)83 public ImapHelper(Context context, PhoneAccountHandle phoneAccount, Network network) { 84 mContext = context; 85 mPhoneAccount = phoneAccount; 86 mNetwork = network; 87 try { 88 TempDirectory.setTempDirectory(context); 89 90 String username = VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context, 91 OmtpConstants.IMAP_USER_NAME, phoneAccount); 92 String password = VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context, 93 OmtpConstants.IMAP_PASSWORD, phoneAccount); 94 String serverName = VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context, 95 OmtpConstants.SERVER_ADDRESS, phoneAccount); 96 int port = Integer.parseInt( 97 VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context, 98 OmtpConstants.IMAP_PORT, phoneAccount)); 99 int auth = ImapStore.FLAG_NONE; 100 101 OmtpVvmCarrierConfigHelper carrierConfigHelper = new OmtpVvmCarrierConfigHelper(context, 102 PhoneUtils.getSubIdForPhoneAccountHandle(phoneAccount)); 103 if (TelephonyManager.VVM_TYPE_CVVM.equals(carrierConfigHelper.getVvmType())) { 104 // TODO: move these into the carrier config app 105 port = 993; 106 auth = ImapStore.FLAG_SSL; 107 } 108 109 mImapStore = new ImapStore( 110 context, this, username, password, port, serverName, auth, network); 111 } catch (NumberFormatException e) { 112 VoicemailUtils.setDataChannelState( 113 mContext, mPhoneAccount, Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION); 114 LogUtils.w(TAG, "Could not parse port number"); 115 } 116 117 mPrefs = PreferenceManager.getDefaultSharedPreferences(context); 118 mQuotaOccupied = mPrefs.getInt(getSharedPrefsKey(PREF_KEY_QUOTA_OCCUPIED), 119 VoicemailContract.Status.QUOTA_UNAVAILABLE); 120 mQuotaTotal = mPrefs.getInt(getSharedPrefsKey(PREF_KEY_QUOTA_TOTAL), 121 VoicemailContract.Status.QUOTA_UNAVAILABLE); 122 123 Log.v(TAG, "Quota:" + mQuotaOccupied + "/" + mQuotaTotal); 124 } 125 126 /** 127 * If mImapStore is null, this means that there was a missing or badly formatted port number, 128 * which means there aren't sufficient credentials for login. If mImapStore is succcessfully 129 * initialized, then ImapHelper is ready to go. 130 */ isSuccessfullyInitialized()131 public boolean isSuccessfullyInitialized() { 132 return mImapStore != null; 133 } 134 isRoaming()135 public boolean isRoaming(){ 136 ConnectivityManager connectivityManager = (ConnectivityManager) mContext.getSystemService( 137 Context.CONNECTIVITY_SERVICE); 138 NetworkInfo info = connectivityManager.getNetworkInfo(mNetwork); 139 if(info == null){ 140 return false; 141 } 142 return info.isRoaming(); 143 } 144 145 /** The caller thread will block until the method returns. */ markMessagesAsRead(List<Voicemail> voicemails)146 public boolean markMessagesAsRead(List<Voicemail> voicemails) { 147 return setFlags(voicemails, Flag.SEEN); 148 } 149 150 /** The caller thread will block until the method returns. */ markMessagesAsDeleted(List<Voicemail> voicemails)151 public boolean markMessagesAsDeleted(List<Voicemail> voicemails) { 152 return setFlags(voicemails, Flag.DELETED); 153 } 154 setDataChannelState(int dataChannelState)155 public void setDataChannelState(int dataChannelState) { 156 VoicemailUtils.setDataChannelState(mContext, mPhoneAccount, dataChannelState); 157 } 158 159 /** 160 * Set flags on the server for a given set of voicemails. 161 * 162 * @param voicemails The voicemails to set flags for. 163 * @param flags The flags to set on the voicemails. 164 * @return {@code true} if the operation completes successfully, {@code false} otherwise. 165 */ setFlags(List<Voicemail> voicemails, String... flags)166 private boolean setFlags(List<Voicemail> voicemails, String... flags) { 167 if (voicemails.size() == 0) { 168 return false; 169 } 170 try { 171 mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE); 172 if (mFolder != null) { 173 mFolder.setFlags(convertToImapMessages(voicemails), flags, true); 174 return true; 175 } 176 return false; 177 } catch (MessagingException e) { 178 LogUtils.e(TAG, e, "Messaging exception"); 179 return false; 180 } finally { 181 closeImapFolder(); 182 } 183 } 184 185 /** 186 * Fetch a list of voicemails from the server. 187 * 188 * @return A list of voicemail objects containing data about voicemails stored on the server. 189 */ fetchAllVoicemails()190 public List<Voicemail> fetchAllVoicemails() { 191 List<Voicemail> result = new ArrayList<Voicemail>(); 192 Message[] messages; 193 try { 194 mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE); 195 if (mFolder == null) { 196 // This means we were unable to successfully open the folder. 197 return null; 198 } 199 200 // This method retrieves lightweight messages containing only the uid of the message. 201 messages = mFolder.getMessages(null); 202 203 for (Message message : messages) { 204 // Get the voicemail details (message structure). 205 MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message); 206 if (messageStructureWrapper != null) { 207 result.add(getVoicemailFromMessageStructure(messageStructureWrapper)); 208 } 209 } 210 return result; 211 } catch (MessagingException e) { 212 LogUtils.e(TAG, e, "Messaging Exception"); 213 return null; 214 } finally { 215 closeImapFolder(); 216 } 217 } 218 219 /** 220 * Extract voicemail details from the message structure. Also fetch transcription if a 221 * transcription exists. 222 */ getVoicemailFromMessageStructure( MessageStructureWrapper messageStructureWrapper)223 private Voicemail getVoicemailFromMessageStructure( 224 MessageStructureWrapper messageStructureWrapper) throws MessagingException{ 225 Message messageDetails = messageStructureWrapper.messageStructure; 226 227 TranscriptionFetchedListener listener = new TranscriptionFetchedListener(); 228 if (messageStructureWrapper.transcriptionBodyPart != null) { 229 FetchProfile fetchProfile = new FetchProfile(); 230 fetchProfile.add(messageStructureWrapper.transcriptionBodyPart); 231 232 mFolder.fetch(new Message[] {messageDetails}, fetchProfile, listener); 233 } 234 235 // Found an audio attachment, this is a valid voicemail. 236 long time = messageDetails.getSentDate().getTime(); 237 String number = getNumber(messageDetails.getFrom()); 238 boolean isRead = Arrays.asList(messageDetails.getFlags()).contains(Flag.SEEN); 239 return Voicemail.createForInsertion(time, number) 240 .setPhoneAccount(mPhoneAccount) 241 .setSourcePackage(mContext.getPackageName()) 242 .setSourceData(messageDetails.getUid()) 243 .setIsRead(isRead) 244 .setTranscription(listener.getVoicemailTranscription()) 245 .build(); 246 } 247 248 /** 249 * The "from" field of a visual voicemail IMAP message is the number of the caller who left 250 * the message. Extract this number from the list of "from" addresses. 251 * 252 * @param fromAddresses A list of addresses that comprise the "from" line. 253 * @return The number of the voicemail sender. 254 */ getNumber(Address[] fromAddresses)255 private String getNumber(Address[] fromAddresses) { 256 if (fromAddresses != null && fromAddresses.length > 0) { 257 if (fromAddresses.length != 1) { 258 LogUtils.w(TAG, "More than one from addresses found. Using the first one."); 259 } 260 String sender = fromAddresses[0].getAddress(); 261 int atPos = sender.indexOf('@'); 262 if (atPos != -1) { 263 // Strip domain part of the address. 264 sender = sender.substring(0, atPos); 265 } 266 return sender; 267 } 268 return null; 269 } 270 271 /** 272 * Fetches the structure of the given message and returns a wrapper containing the message 273 * structure and the transcription structure (if applicable). 274 * 275 * @throws MessagingException if fetching the structure of the message fails 276 */ fetchMessageStructure(Message message)277 private MessageStructureWrapper fetchMessageStructure(Message message) 278 throws MessagingException { 279 LogUtils.d(TAG, "Fetching message structure for " + message.getUid()); 280 281 MessageStructureFetchedListener listener = new MessageStructureFetchedListener(); 282 283 FetchProfile fetchProfile = new FetchProfile(); 284 fetchProfile.addAll(Arrays.asList(FetchProfile.Item.FLAGS, FetchProfile.Item.ENVELOPE, 285 FetchProfile.Item.STRUCTURE)); 286 287 // The IMAP folder fetch method will call "messageRetrieved" on the listener when the 288 // message is successfully retrieved. 289 mFolder.fetch(new Message[] {message}, fetchProfile, listener); 290 return listener.getMessageStructure(); 291 } 292 fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid)293 public boolean fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid) { 294 try { 295 mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE); 296 if (mFolder == null) { 297 // This means we were unable to successfully open the folder. 298 return false; 299 } 300 Message message = mFolder.getMessage(uid); 301 if (message == null) { 302 return false; 303 } 304 VoicemailPayload voicemailPayload = fetchVoicemailPayload(message); 305 306 if (voicemailPayload == null) { 307 return false; 308 } 309 310 callback.setVoicemailContent(voicemailPayload); 311 return true; 312 } catch (MessagingException e) { 313 } finally { 314 closeImapFolder(); 315 } 316 return false; 317 } 318 319 /** 320 * Fetches the body of the given message and returns the parsed voicemail payload. 321 * 322 * @throws MessagingException if fetching the body of the message fails 323 */ fetchVoicemailPayload(Message message)324 private VoicemailPayload fetchVoicemailPayload(Message message) 325 throws MessagingException { 326 LogUtils.d(TAG, "Fetching message body for " + message.getUid()); 327 328 MessageBodyFetchedListener listener = new MessageBodyFetchedListener(); 329 330 FetchProfile fetchProfile = new FetchProfile(); 331 fetchProfile.add(FetchProfile.Item.BODY); 332 333 mFolder.fetch(new Message[] {message}, fetchProfile, listener); 334 return listener.getVoicemailPayload(); 335 } 336 fetchTranscription(TranscriptionFetchedCallback callback, String uid)337 public boolean fetchTranscription(TranscriptionFetchedCallback callback, String uid) { 338 try { 339 mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE); 340 if (mFolder == null) { 341 // This means we were unable to successfully open the folder. 342 return false; 343 } 344 345 Message message = mFolder.getMessage(uid); 346 if (message == null) { 347 return false; 348 } 349 350 MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message); 351 if (messageStructureWrapper != null) { 352 TranscriptionFetchedListener listener = new TranscriptionFetchedListener(); 353 if (messageStructureWrapper.transcriptionBodyPart != null) { 354 FetchProfile fetchProfile = new FetchProfile(); 355 fetchProfile.add(messageStructureWrapper.transcriptionBodyPart); 356 357 // This method is called synchronously so the transcription will be populated 358 // in the listener once the next method is called. 359 mFolder.fetch(new Message[] {message}, fetchProfile, listener); 360 callback.setVoicemailTranscription(listener.getVoicemailTranscription()); 361 } 362 } 363 return true; 364 } catch (MessagingException e) { 365 LogUtils.e(TAG, e, "Messaging Exception"); 366 return false; 367 } finally { 368 closeImapFolder(); 369 } 370 } 371 updateQuota()372 public void updateQuota() { 373 try { 374 mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE); 375 if (mFolder == null) { 376 // This means we were unable to successfully open the folder. 377 return; 378 } 379 updateQuota(mFolder); 380 } catch (MessagingException e) { 381 LogUtils.e(TAG, e, "Messaging Exception"); 382 } finally { 383 closeImapFolder(); 384 } 385 } 386 updateQuota(ImapFolder folder)387 private void updateQuota(ImapFolder folder) throws MessagingException { 388 setQuota(folder.getQuota()); 389 } 390 setQuota(ImapFolder.Quota quota)391 private void setQuota(ImapFolder.Quota quota) { 392 if (quota == null) { 393 return; 394 } 395 if (quota.occupied == mQuotaOccupied && quota.total == mQuotaTotal) { 396 Log.v(TAG, "Quota hasn't changed"); 397 return; 398 } 399 mQuotaOccupied = quota.occupied; 400 mQuotaTotal = quota.total; 401 VoicemailContract.Status 402 .setQuota(mContext, mPhoneAccount, mQuotaOccupied, mQuotaTotal); 403 mPrefs.edit() 404 .putInt(getSharedPrefsKey(PREF_KEY_QUOTA_OCCUPIED), mQuotaOccupied) 405 .putInt(getSharedPrefsKey(PREF_KEY_QUOTA_TOTAL), mQuotaTotal) 406 .apply(); 407 Log.v(TAG, "Quota changed to " + mQuotaOccupied + "/" + mQuotaTotal); 408 } 409 /** 410 * A wrapper to hold a message with its header details and the structure for transcriptions 411 * (so they can be fetched in the future). 412 */ 413 public class MessageStructureWrapper { 414 public Message messageStructure; 415 public BodyPart transcriptionBodyPart; 416 MessageStructureWrapper()417 public MessageStructureWrapper() { } 418 } 419 420 /** 421 * Listener for the message structure being fetched. 422 */ 423 private final class MessageStructureFetchedListener 424 implements ImapFolder.MessageRetrievalListener { 425 private MessageStructureWrapper mMessageStructure; 426 MessageStructureFetchedListener()427 public MessageStructureFetchedListener() { 428 } 429 getMessageStructure()430 public MessageStructureWrapper getMessageStructure() { 431 return mMessageStructure; 432 } 433 434 @Override messageRetrieved(Message message)435 public void messageRetrieved(Message message) { 436 LogUtils.d(TAG, "Fetched message structure for " + message.getUid()); 437 LogUtils.d(TAG, "Message retrieved: " + message); 438 try { 439 mMessageStructure = getMessageOrNull(message); 440 if (mMessageStructure == null) { 441 LogUtils.d(TAG, "This voicemail does not have an attachment..."); 442 return; 443 } 444 } catch (MessagingException e) { 445 LogUtils.e(TAG, e, "Messaging Exception"); 446 closeImapFolder(); 447 } 448 } 449 450 /** 451 * Check if this IMAP message is a valid voicemail and whether it contains a transcription. 452 * 453 * @param message The IMAP message. 454 * @return The MessageStructureWrapper object corresponding to an IMAP message and 455 * transcription. 456 * @throws MessagingException 457 */ getMessageOrNull(Message message)458 private MessageStructureWrapper getMessageOrNull(Message message) 459 throws MessagingException { 460 if (!message.getMimeType().startsWith("multipart/")) { 461 LogUtils.w(TAG, "Ignored non multi-part message"); 462 return null; 463 } 464 465 MessageStructureWrapper messageStructureWrapper = new MessageStructureWrapper(); 466 467 Multipart multipart = (Multipart) message.getBody(); 468 for (int i = 0; i < multipart.getCount(); ++i) { 469 BodyPart bodyPart = multipart.getBodyPart(i); 470 String bodyPartMimeType = bodyPart.getMimeType().toLowerCase(); 471 LogUtils.d(TAG, "bodyPart mime type: " + bodyPartMimeType); 472 473 if (bodyPartMimeType.startsWith("audio/")) { 474 messageStructureWrapper.messageStructure = message; 475 } else if (bodyPartMimeType.startsWith("text/")) { 476 messageStructureWrapper.transcriptionBodyPart = bodyPart; 477 } 478 } 479 480 if (messageStructureWrapper.messageStructure != null) { 481 return messageStructureWrapper; 482 } 483 484 // No attachment found, this is not a voicemail. 485 return null; 486 } 487 } 488 489 /** 490 * Listener for the message body being fetched. 491 */ 492 private final class MessageBodyFetchedListener implements ImapFolder.MessageRetrievalListener { 493 private VoicemailPayload mVoicemailPayload; 494 495 /** Returns the fetch voicemail payload. */ getVoicemailPayload()496 public VoicemailPayload getVoicemailPayload() { 497 return mVoicemailPayload; 498 } 499 500 @Override messageRetrieved(Message message)501 public void messageRetrieved(Message message) { 502 LogUtils.d(TAG, "Fetched message body for " + message.getUid()); 503 LogUtils.d(TAG, "Message retrieved: " + message); 504 try { 505 mVoicemailPayload = getVoicemailPayloadFromMessage(message); 506 } catch (MessagingException e) { 507 LogUtils.e(TAG, "Messaging Exception:", e); 508 } catch (IOException e) { 509 LogUtils.e(TAG, "IO Exception:", e); 510 } 511 } 512 getVoicemailPayloadFromMessage(Message message)513 private VoicemailPayload getVoicemailPayloadFromMessage(Message message) 514 throws MessagingException, IOException { 515 Multipart multipart = (Multipart) message.getBody(); 516 for (int i = 0; i < multipart.getCount(); ++i) { 517 BodyPart bodyPart = multipart.getBodyPart(i); 518 String bodyPartMimeType = bodyPart.getMimeType().toLowerCase(); 519 LogUtils.d(TAG, "bodyPart mime type: " + bodyPartMimeType); 520 521 if (bodyPartMimeType.startsWith("audio/")) { 522 byte[] bytes = getDataFromBody(bodyPart.getBody()); 523 LogUtils.d(TAG, String.format("Fetched %s bytes of data", bytes.length)); 524 return new VoicemailPayload(bodyPartMimeType, bytes); 525 } 526 } 527 LogUtils.e(TAG, "No audio attachment found on this voicemail"); 528 return null; 529 } 530 } 531 532 /** 533 * Listener for the transcription being fetched. 534 */ 535 private final class TranscriptionFetchedListener implements 536 ImapFolder.MessageRetrievalListener { 537 private String mVoicemailTranscription; 538 539 /** Returns the fetched voicemail transcription. */ getVoicemailTranscription()540 public String getVoicemailTranscription() { 541 return mVoicemailTranscription; 542 } 543 544 @Override messageRetrieved(Message message)545 public void messageRetrieved(Message message) { 546 LogUtils.d(TAG, "Fetched transcription for " + message.getUid()); 547 try { 548 mVoicemailTranscription = new String(getDataFromBody(message.getBody())); 549 } catch (MessagingException e) { 550 LogUtils.e(TAG, "Messaging Exception:", e); 551 } catch (IOException e) { 552 LogUtils.e(TAG, "IO Exception:", e); 553 } 554 } 555 } 556 openImapFolder(String modeReadWrite)557 private ImapFolder openImapFolder(String modeReadWrite) { 558 try { 559 if (mImapStore == null) { 560 return null; 561 } 562 ImapFolder folder = new ImapFolder(mImapStore, ImapConstants.INBOX); 563 folder.open(modeReadWrite); 564 return folder; 565 } catch (MessagingException e) { 566 LogUtils.e(TAG, e, "Messaging Exception"); 567 } 568 return null; 569 } 570 convertToImapMessages(List<Voicemail> voicemails)571 private Message[] convertToImapMessages(List<Voicemail> voicemails) { 572 Message[] messages = new Message[voicemails.size()]; 573 for (int i = 0; i < voicemails.size(); ++i) { 574 messages[i] = new MimeMessage(); 575 messages[i].setUid(voicemails.get(i).getSourceData()); 576 } 577 return messages; 578 } 579 closeImapFolder()580 private void closeImapFolder() { 581 if (mFolder != null) { 582 mFolder.close(true); 583 } 584 } 585 getDataFromBody(Body body)586 private byte[] getDataFromBody(Body body) throws IOException, MessagingException { 587 ByteArrayOutputStream out = new ByteArrayOutputStream(); 588 BufferedOutputStream bufferedOut = new BufferedOutputStream(out); 589 try { 590 body.writeTo(bufferedOut); 591 return Base64.decode(out.toByteArray(), Base64.DEFAULT); 592 } finally { 593 IoUtils.closeQuietly(bufferedOut); 594 IoUtils.closeQuietly(out); 595 } 596 } 597 getSharedPrefsKey(String key)598 private String getSharedPrefsKey(String key) { 599 return VisualVoicemailSettingsUtil.getVisualVoicemailSharedPrefsKey(key, mPhoneAccount); 600 } 601 }